Files
openclaw/packages/gateway-protocol/src/validation-errors.ts
Peter Steinberger 1bcc4c5e70 feat(gateway): add cooperative host suspension (#103618)
* feat(gateway): add cooperative suspension preparation

* style: satisfy suspension lint checks

* test(gateway): reset work admission between shared suites

* fix(gateway): reject upgrades during suspension

* fix(gateway): preserve admitted work during suspension

* test(gateway): isolate suspension and restart state

* fix(gateway): close suspension false-ready gaps

* refactor(protocol): slim suspension declaration graph

* refactor(plugin-sdk): sever protocol registry edges

* fix(gateway): preserve admitted restart follow-ups

* fix(gateway): make suspension recovery fail closed

* fix(protocol): keep validation formatter re-export only

* test(gateway): simplify deferred fixture type

* style(gateway): clarify suspension entry name

* fix(gateway): retain detached work admission
2026-07-10 20:24:53 +01:00

77 lines
2.8 KiB
TypeScript

/** Normalized validation error shape exposed by every protocol validator. */
export type ValidationError = {
/** Failed schema keyword, when the validator can report one. */
keyword?: string;
/** JSON-pointer path to the failing data location. */
instancePath?: string;
/** JSON-pointer path to the failing schema location. */
schemaPath?: string;
/** Validator-specific keyword parameters for richer diagnostics. */
params?: Record<string, unknown>;
/** Human-readable validation message. */
message?: string;
};
function firstStringParam(value: unknown): string | undefined {
if (typeof value === "string" && value.trim()) {
return value;
}
if (Array.isArray(value)) {
return value.find(
(entry): entry is string => typeof entry === "string" && entry.trim().length > 0,
);
}
return undefined;
}
/** Convert validator errors into compact operator-facing failure text. */
export function formatValidationErrors(errors: ValidationError[] | null | undefined) {
if (!errors?.length) {
return "unknown validation error";
}
const parts: string[] = [];
for (const err of errors) {
const keyword = typeof err?.keyword === "string" ? err.keyword : "";
const instancePath = typeof err?.instancePath === "string" ? err.instancePath : "";
if (keyword === "additionalProperties") {
const additionalProperty =
firstStringParam(err?.params?.additionalProperty) ??
firstStringParam(err?.params?.additionalProperties);
if (additionalProperty) {
const where = instancePath ? `at ${instancePath}` : "at root";
parts.push(`${where}: unexpected property '${additionalProperty}'`);
continue;
}
}
if (keyword === "required") {
const missingProperty =
firstStringParam(err?.params?.missingProperty) ??
firstStringParam(err?.params?.requiredProperties);
if (missingProperty) {
const where = instancePath ? `at ${instancePath}: ` : "";
parts.push(`${where}must have required property '${missingProperty}'`);
continue;
}
}
const failingKeyword =
typeof err?.params?.failingKeyword === "string" ? err.params.failingKeyword : "";
// TypeBox reports conditional required-property misses through if/then
// keywords, which otherwise hide the actionable missing-property context.
const message =
keyword === "then" || (keyword === "if" && failingKeyword === "then")
? "must have required conditional properties"
: typeof err?.message === "string" && err.message.trim()
? err.message
: "validation error";
const where = instancePath ? `at ${instancePath}: ` : "";
parts.push(`${where}${message}`);
}
const unique = [...new Set(parts.filter((part) => part.trim()))];
return unique.length > 0 ? unique.join("; ") : "unknown validation error";
}