mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 22:17:00 +00:00
* fix(infra): unify environment truthiness * fix(infra): unify missing path classification * refactor(infra): unify realpath fallbacks * fix(infra): unify abortable sleep errors * docs(infra): clarify path fallback semantics * fix(infra): route realpaths through policy wrapper * fix(infra): ratchet plugin SDK wildcard budget * fix(agents): preserve zero-delay abort precedence * fix(infra): preserve fallback and media recovery contracts * fix(plugins): share quarantine path resolution
190 lines
6.7 KiB
TypeScript
190 lines
6.7 KiB
TypeScript
// Tests shared infra error formatting helpers.
|
|
import { describe, expect, it } from "vitest";
|
|
import { collectNestedErrorCandidates, extractErrorCodeOrErrno } from "./error-graph-internal.js";
|
|
import {
|
|
collectErrorGraphCandidates,
|
|
extractErrorCode,
|
|
formatErrorMessage,
|
|
formatUncaughtError,
|
|
hasErrnoCode,
|
|
isErrno,
|
|
isMissingPathError,
|
|
readErrorName,
|
|
} from "./errors.js";
|
|
|
|
function createCircularObject() {
|
|
const circular: { self?: unknown } = {};
|
|
circular.self = circular;
|
|
return circular;
|
|
}
|
|
|
|
describe("error helpers", () => {
|
|
it.each([
|
|
{ value: { code: "EADDRINUSE" }, expected: "EADDRINUSE" },
|
|
{ value: { code: 429 }, expected: "429" },
|
|
{ value: { code: false }, expected: undefined },
|
|
{ value: "boom", expected: undefined },
|
|
])("extracts error codes from %j", ({ value, expected }) => {
|
|
expect(extractErrorCode(value)).toBe(expected);
|
|
});
|
|
|
|
it.each([
|
|
{ value: { name: "AbortError" }, expected: "AbortError" },
|
|
{ value: { name: 42 }, expected: "" },
|
|
{ value: null, expected: "" },
|
|
])("reads error names from %j", ({ value, expected }) => {
|
|
expect(readErrorName(value)).toBe(expected);
|
|
});
|
|
|
|
it("walks nested error graphs once in breadth-first order", () => {
|
|
const leaf = { name: "leaf" };
|
|
const child = { name: "child" } as {
|
|
name: string;
|
|
cause?: unknown;
|
|
errors?: unknown[];
|
|
};
|
|
const root = { name: "root", cause: child, errors: [leaf, child] };
|
|
child.cause = root;
|
|
|
|
expect(
|
|
collectErrorGraphCandidates(root, (current) => [
|
|
current.cause,
|
|
...((current as { errors?: unknown[] }).errors ?? []),
|
|
]),
|
|
).toEqual([root, child, leaf]);
|
|
expect(collectErrorGraphCandidates(null)).toStrictEqual([]);
|
|
});
|
|
|
|
it("walks every canonical wrapper edge once despite duplicates and cycles", () => {
|
|
const cause = { name: "cause" } as { name: string; cause?: unknown };
|
|
const reason = { name: "reason" };
|
|
const original = { name: "original" };
|
|
const error = { name: "error" };
|
|
const data = { name: "data" };
|
|
const aggregate = { name: "aggregate" };
|
|
const root = {
|
|
name: "root",
|
|
cause,
|
|
reason,
|
|
original,
|
|
error,
|
|
data,
|
|
errors: [aggregate, cause],
|
|
};
|
|
cause.cause = root;
|
|
|
|
expect(collectNestedErrorCandidates(root)).toEqual([
|
|
root,
|
|
cause,
|
|
reason,
|
|
original,
|
|
error,
|
|
data,
|
|
aggregate,
|
|
]);
|
|
});
|
|
|
|
it.each([
|
|
{ value: { code: " econnreset " }, expected: "ECONNRESET" },
|
|
{ value: { errno: " eai_again " }, expected: "EAI_AGAIN" },
|
|
{ value: { errno: -3001 }, expected: "-3001" },
|
|
{ value: { errno: false }, expected: undefined },
|
|
])("normalizes error code or errno from %#", ({ value, expected }) => {
|
|
expect(extractErrorCodeOrErrno(value)).toBe(expected);
|
|
});
|
|
|
|
it("matches errno-shaped errors by code", () => {
|
|
const err = Object.assign(new Error("busy"), { code: "EADDRINUSE" });
|
|
expect(isErrno(err)).toBe(true);
|
|
expect(hasErrnoCode(err, "EADDRINUSE")).toBe(true);
|
|
expect(hasErrnoCode(err, "ENOENT")).toBe(false);
|
|
expect(isErrno("busy")).toBe(false);
|
|
});
|
|
|
|
it.each(["ENOENT", "ENOTDIR", "not-found"])(
|
|
"classifies %s as a missing path without requiring Error identity",
|
|
(code) => {
|
|
expect(isMissingPathError({ code })).toBe(true);
|
|
},
|
|
);
|
|
|
|
it("does not classify other fs-safe or errno failures as missing paths", () => {
|
|
expect(isMissingPathError({ code: "path-alias" })).toBe(false);
|
|
expect(isMissingPathError(new Error("ENOENT"))).toBe(false);
|
|
});
|
|
|
|
it.each([
|
|
{ value: 123n, expected: "123" },
|
|
{ value: false, expected: "false" },
|
|
{ value: createCircularObject(), expected: "[object Object]" },
|
|
])("formats error messages for case %#", ({ value, expected }) => {
|
|
expect(formatErrorMessage(value)).toBe(expected);
|
|
});
|
|
|
|
it("traverses .cause chain to include nested error messages", () => {
|
|
const rootCause = new Error("ECONNRESET");
|
|
const httpError = Object.assign(new Error("Network request for 'sendMessage' failed!"), {
|
|
cause: rootCause,
|
|
});
|
|
const formatted = formatErrorMessage(httpError);
|
|
expect(formatted).toContain("Network request for 'sendMessage' failed!");
|
|
expect(formatted).toContain("ECONNRESET");
|
|
});
|
|
|
|
it("handles circular .cause references without infinite loop", () => {
|
|
const a: Error & { cause?: unknown } = new Error("error A");
|
|
const b: Error & { cause?: unknown } = new Error("error B");
|
|
a.cause = b;
|
|
b.cause = a;
|
|
const formatted = formatErrorMessage(a);
|
|
expect(formatted).toBe("error A | error B");
|
|
});
|
|
|
|
it("dedupes repeated cause messages while preserving deeper distinct causes", () => {
|
|
const rootCause = new Error("provider auth lookup failed");
|
|
const inner = new Error('No API key found for provider "openai".', { cause: rootCause });
|
|
const wrapper = new Error(inner.message, { cause: inner });
|
|
expect(formatErrorMessage(wrapper)).toBe(`${inner.message} | ${rootCause.message}`);
|
|
});
|
|
|
|
it("redacts sensitive tokens from formatted error messages", () => {
|
|
const token = "sk-abcdefghijklmnopqrstuv";
|
|
const formatted = formatErrorMessage(new Error(`Authorization: Bearer ${token}`));
|
|
expect(formatted).toContain("Authorization: Bearer");
|
|
expect(formatted).not.toContain(token);
|
|
});
|
|
|
|
it("redacts HTTP client config secrets from formatted error chains", () => {
|
|
const appSecret = "feishu_app_secret_1234567890";
|
|
const tenantToken = "feishu_tenant_access_abcdef123456";
|
|
const rootCause = new Error(
|
|
`request config: { appSecret: '${appSecret}', headers: { authorization: 'Bearer ${tenantToken}' } }`,
|
|
);
|
|
const httpError = Object.assign(new Error(`POST /auth/v3/tenant_access_token failed`), {
|
|
cause: rootCause,
|
|
});
|
|
|
|
const formatted = formatErrorMessage(httpError);
|
|
|
|
expect(formatted).toContain("POST /auth/v3/tenant_access_token failed");
|
|
expect(formatted).toContain("appSecret:");
|
|
expect(formatted).toContain("authorization:");
|
|
expect(formatted).not.toContain(appSecret);
|
|
expect(formatted).not.toContain(tenantToken);
|
|
});
|
|
|
|
it("uses message-only formatting for INVALID_CONFIG and stack formatting otherwise", () => {
|
|
const invalidConfig = Object.assign(new Error("TOKEN=sk-abcdefghijklmnopqrstuv"), {
|
|
code: "INVALID_CONFIG",
|
|
stack: "Error: TOKEN=sk-abcdefghijklmnopqrstuv\n at ignored",
|
|
});
|
|
expect(formatUncaughtError(invalidConfig)).not.toContain("at ignored");
|
|
|
|
const uncaught = new Error("boom");
|
|
uncaught.stack = "Error: Authorization: Bearer sk-abcdefghijklmnopqrstuv\n at runTask";
|
|
const formatted = formatUncaughtError(uncaught);
|
|
expect(formatted).toContain("at runTask");
|
|
expect(formatted).not.toContain("sk-abcdefghijklmnopqrstuv");
|
|
});
|
|
});
|