Gateway: surface config validation issues

This commit is contained in:
huntharo 2026-03-10 21:45:38 -04:00
parent 4eccea9f7f
commit 9a5eab0019
No known key found for this signature in database
2 changed files with 52 additions and 3 deletions

View File

@ -10,6 +10,7 @@ import {
validateConfigObjectWithPlugins,
writeConfigFile,
} from "../../config/config.js";
import { formatConfigIssueLines } from "../../config/issue-format.js";
import { applyLegacyMigrations } from "../../config/legacy.js";
import { applyMergePatch } from "../../config/merge-patch.js";
import {
@ -23,7 +24,7 @@ import {
type ConfigSchemaResponse,
} from "../../config/schema.js";
import { extractDeliveryInfo } from "../../config/sessions.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import type { ConfigValidationIssue, OpenClawConfig } from "../../config/types.openclaw.js";
import {
formatDoctorNonInteractiveHint,
type RestartSentinelPayload,
@ -54,6 +55,8 @@ import { parseRestartRequestParams } from "./restart-request.js";
import type { GatewayRequestHandlers, RespondFn } from "./types.js";
import { assertValidParams } from "./validation.js";
const MAX_CONFIG_ISSUES_IN_ERROR_MESSAGE = 3;
function requireConfigBaseHash(
params: unknown,
snapshot: Awaited<ReturnType<typeof readConfigFileSnapshot>>,
@ -158,7 +161,7 @@ function parseValidateConfigFromRawOrRespond(
respond(
false,
undefined,
errorShape(ErrorCodes.INVALID_REQUEST, "invalid config", {
errorShape(ErrorCodes.INVALID_REQUEST, summarizeConfigValidationIssues(validated.issues), {
details: { issues: validated.issues },
}),
);
@ -167,6 +170,20 @@ function parseValidateConfigFromRawOrRespond(
return { config: validated.config, schema };
}
function summarizeConfigValidationIssues(issues: ReadonlyArray<ConfigValidationIssue>): string {
const trimmed = issues.slice(0, MAX_CONFIG_ISSUES_IN_ERROR_MESSAGE);
const lines = formatConfigIssueLines(trimmed, "", { normalizeRoot: true })
.map((line) => line.trim())
.filter(Boolean);
if (lines.length === 0) {
return "invalid config";
}
const hiddenCount = Math.max(0, issues.length - lines.length);
return `invalid config: ${lines.join("; ")}${
hiddenCount > 0 ? ` (+${hiddenCount} more issue${hiddenCount === 1 ? "" : "s"})` : ""
}`;
}
function resolveConfigRestartRequest(params: unknown): {
sessionKey: string | undefined;
note: string | undefined;
@ -398,7 +415,7 @@ export const configHandlers: GatewayRequestHandlers = {
respond(
false,
undefined,
errorShape(ErrorCodes.INVALID_REQUEST, "invalid config", {
errorShape(ErrorCodes.INVALID_REQUEST, summarizeConfigValidationIssues(validated.issues), {
details: { issues: validated.issues },
}),
);

View File

@ -72,6 +72,38 @@ describe("gateway config methods", () => {
expect(res.payload?.config).toBeTruthy();
});
it("returns config.set validation details in the top-level error message", async () => {
const current = await rpcReq<{
hash?: string;
}>(requireWs(), "config.get", {});
expect(current.ok).toBe(true);
expect(typeof current.payload?.hash).toBe("string");
const res = await rpcReq<{
ok?: boolean;
error?: {
message?: string;
};
}>(requireWs(), "config.set", {
raw: JSON.stringify({ gateway: { bind: 123 } }),
baseHash: current.payload?.hash,
});
const error = res.error as
| {
message?: string;
details?: {
issues?: Array<{ path?: string; message?: string }>;
};
}
| undefined;
expect(res.ok).toBe(false);
expect(error?.message ?? "").toContain("invalid config:");
expect(error?.message ?? "").toContain("gateway.bind");
expect(error?.message ?? "").toContain("allowed:");
expect(error?.details?.issues?.[0]?.path).toBe("gateway.bind");
});
it("returns a path-scoped config schema lookup", async () => {
const res = await rpcReq<{
path: string;