fix(gateway): improve validation errors (#1347)
Thanks @vignesh07. Co-authored-by: Vignesh <vignesh07@users.noreply.github.com>
This commit is contained in:
@@ -29,6 +29,7 @@ Docs: https://docs.clawd.bot
|
|||||||
- Model catalog: avoid caching import failures, log transient discovery errors, and keep partial results. (#1332) — thanks @dougvk.
|
- Model catalog: avoid caching import failures, log transient discovery errors, and keep partial results. (#1332) — thanks @dougvk.
|
||||||
- Doctor: clarify plugin auto-enable hint text in the startup banner.
|
- Doctor: clarify plugin auto-enable hint text in the startup banner.
|
||||||
- Gateway: clarify unauthorized handshake responses with token/password mismatch guidance.
|
- Gateway: clarify unauthorized handshake responses with token/password mismatch guidance.
|
||||||
|
- Gateway: clarify connect/validation errors for gateway params. (#1347) — thanks @vignesh07.
|
||||||
- Gateway: preserve restart wake routing + thread replies across restarts. (#1337) — thanks @John-Rood.
|
- Gateway: preserve restart wake routing + thread replies across restarts. (#1337) — thanks @John-Rood.
|
||||||
- Gateway: reschedule per-agent heartbeats on config hot reload without restarting the runner.
|
- Gateway: reschedule per-agent heartbeats on config hot reload without restarting the runner.
|
||||||
- Config: log invalid config issues once per run and keep invalid-config errors stackless.
|
- Config: log invalid config issues once per run and keep invalid-config errors stackless.
|
||||||
|
|||||||
65
src/gateway/protocol/index.test.ts
Normal file
65
src/gateway/protocol/index.test.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import type { ErrorObject } from "ajv";
|
||||||
|
|
||||||
|
import { formatValidationErrors } from "./index.js";
|
||||||
|
|
||||||
|
const makeError = (overrides: Partial<ErrorObject>): ErrorObject => ({
|
||||||
|
keyword: "type",
|
||||||
|
instancePath: "",
|
||||||
|
schemaPath: "#/",
|
||||||
|
params: {},
|
||||||
|
message: "validation error",
|
||||||
|
...overrides,
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("formatValidationErrors", () => {
|
||||||
|
it("returns unknown validation error when missing errors", () => {
|
||||||
|
expect(formatValidationErrors(undefined)).toBe("unknown validation error");
|
||||||
|
expect(formatValidationErrors(null)).toBe("unknown validation error");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns unknown validation error when errors list is empty", () => {
|
||||||
|
expect(formatValidationErrors([])).toBe("unknown validation error");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("formats additionalProperties at root", () => {
|
||||||
|
const err = makeError({
|
||||||
|
keyword: "additionalProperties",
|
||||||
|
params: { additionalProperty: "token" },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(formatValidationErrors([err])).toBe("at root: unexpected property 'token'");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("formats additionalProperties with instancePath", () => {
|
||||||
|
const err = makeError({
|
||||||
|
keyword: "additionalProperties",
|
||||||
|
instancePath: "/auth",
|
||||||
|
params: { additionalProperty: "token" },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(formatValidationErrors([err])).toBe("at /auth: unexpected property 'token'");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("formats message with path for other errors", () => {
|
||||||
|
const err = makeError({
|
||||||
|
keyword: "required",
|
||||||
|
instancePath: "/auth",
|
||||||
|
message: "must have required property 'token'",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(formatValidationErrors([err])).toBe("at /auth: must have required property 'token'");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("de-dupes repeated entries", () => {
|
||||||
|
const err = makeError({
|
||||||
|
keyword: "required",
|
||||||
|
instancePath: "/auth",
|
||||||
|
message: "must have required property 'token'",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(formatValidationErrors([err, err])).toBe(
|
||||||
|
"at /auth: must have required property 'token'",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -310,7 +310,7 @@ export const validateWebLoginStartParams =
|
|||||||
export const validateWebLoginWaitParams = ajv.compile<WebLoginWaitParams>(WebLoginWaitParamsSchema);
|
export const validateWebLoginWaitParams = ajv.compile<WebLoginWaitParams>(WebLoginWaitParamsSchema);
|
||||||
|
|
||||||
export function formatValidationErrors(errors: ErrorObject[] | null | undefined) {
|
export function formatValidationErrors(errors: ErrorObject[] | null | undefined) {
|
||||||
if (!errors) return "unknown validation error";
|
if (!errors?.length) return "unknown validation error";
|
||||||
|
|
||||||
const parts: string[] = [];
|
const parts: string[] = [];
|
||||||
|
|
||||||
@@ -328,13 +328,18 @@ export function formatValidationErrors(errors: ErrorObject[] | null | undefined)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const message = typeof err?.message === "string" ? err.message : "validation error";
|
const message =
|
||||||
|
typeof err?.message === "string" && err.message.trim() ? err.message : "validation error";
|
||||||
const where = instancePath ? `at ${instancePath}: ` : "";
|
const where = instancePath ? `at ${instancePath}: ` : "";
|
||||||
parts.push(`${where}${message}`);
|
parts.push(`${where}${message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// De-dupe while preserving order.
|
// De-dupe while preserving order.
|
||||||
const unique = Array.from(new Set(parts));
|
const unique = Array.from(new Set(parts.filter((part) => part.trim())));
|
||||||
|
if (!unique.length) {
|
||||||
|
const fallback = ajv.errorsText(errors, { separator: "; " });
|
||||||
|
return fallback || "unknown validation error";
|
||||||
|
}
|
||||||
return unique.join("; ");
|
return unique.join("; ");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user