mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 19:31:35 +00:00
* 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
77 lines
2.8 KiB
TypeScript
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";
|
|
}
|