Files
openclaw/packages/acp-core/src/runtime/errors.test.ts
2026-06-04 20:54:41 -04:00

193 lines
6.1 KiB
TypeScript

// ACP Core tests cover errors behavior.
import { afterEach, describe, expect, it } from "vitest";
import { configureAcpErrorRedactor } from "../error-format.js";
import {
AcpRuntimeError,
formatAcpErrorChain,
isAcpRuntimeError,
toAcpRuntimeError,
withAcpRuntimeErrorBoundary,
} from "./errors.js";
async function expectRejectedAcpRuntimeError(promise: Promise<unknown>): Promise<AcpRuntimeError> {
try {
await promise;
} catch (error) {
expect(error).toBeInstanceOf(AcpRuntimeError);
return error as AcpRuntimeError;
}
throw new Error("expected ACP runtime error rejection");
}
afterEach(() => {
configureAcpErrorRedactor(undefined);
});
describe("withAcpRuntimeErrorBoundary", () => {
it("wraps generic errors with fallback code and source message", async () => {
const sourceError = new Error("boom");
const error = await expectRejectedAcpRuntimeError(
withAcpRuntimeErrorBoundary({
run: async () => {
throw sourceError;
},
fallbackCode: "ACP_TURN_FAILED",
fallbackMessage: "fallback",
}),
);
expect(error.name).toBe("AcpRuntimeError");
expect(error.code).toBe("ACP_TURN_FAILED");
expect(error.message).toBe("boom");
expect(error.cause).toBe(sourceError);
});
it("passes through existing ACP runtime errors", async () => {
const existing = new AcpRuntimeError("ACP_BACKEND_MISSING", "backend missing");
await expect(
withAcpRuntimeErrorBoundary({
run: async () => {
throw existing;
},
fallbackCode: "ACP_TURN_FAILED",
fallbackMessage: "fallback",
}),
).rejects.toBe(existing);
});
it("preserves ACP runtime codes from foreign package errors", async () => {
class ForeignAcpRuntimeError extends Error {
readonly code = "ACP_BACKEND_MISSING" as const;
}
const foreignError = new ForeignAcpRuntimeError("backend missing");
const error = await expectRejectedAcpRuntimeError(
withAcpRuntimeErrorBoundary({
run: async () => {
throw foreignError;
},
fallbackCode: "ACP_TURN_FAILED",
fallbackMessage: "fallback",
}),
);
expect(error.name).toBe("AcpRuntimeError");
expect(error.code).toBe("ACP_BACKEND_MISSING");
expect(error.message).toBe("backend missing");
expect(error.cause).toBe(foreignError);
expect(isAcpRuntimeError(foreignError)).toBe(true);
});
it("preserves redacted RequestError details from numeric ACP errors", () => {
const token = "sk-abcdefghijklmnopqrstuvwxyz123456";
const requestError = Object.assign(new Error("Internal error"), {
name: "RequestError",
code: -32603,
data: {
details: `unknown config option: timeout; token=${token}`,
},
});
const error = toAcpRuntimeError({
error: requestError,
fallbackCode: "ACP_TURN_FAILED",
fallbackMessage: "fallback",
});
expect(error.code).toBe("ACP_TURN_FAILED");
expect(error.message).toContain("Internal error: unknown config option: timeout");
expect(error.message).not.toContain(token);
expect(error.cause).toBe(requestError);
});
it("keeps foreign OpenClaw ACP string code behavior unchanged", () => {
const foreignError = Object.assign(new Error("backend missing"), {
code: "ACP_BACKEND_MISSING",
data: {
details: "extra backend diagnostic",
},
});
const error = toAcpRuntimeError({
error: foreignError,
fallbackCode: "ACP_TURN_FAILED",
fallbackMessage: "fallback",
});
expect(error.code).toBe("ACP_BACKEND_MISSING");
expect(error.message).toBe("backend missing");
expect(error.cause).toBe(foreignError);
});
it("keeps generic non-RequestError messages unchanged", () => {
const sourceError = Object.assign(new Error("boom"), {
data: {
details: "extra diagnostic",
},
});
const error = toAcpRuntimeError({
error: sourceError,
fallbackCode: "ACP_TURN_FAILED",
fallbackMessage: "fallback",
});
expect(error.code).toBe("ACP_TURN_FAILED");
expect(error.message).toBe("boom");
expect(error.cause).toBe(sourceError);
});
});
describe("formatAcpErrorChain redaction", () => {
it("redacts secret-shaped tokens that arrive as top-level non-Error values", () => {
const token = "sk-abcdefghijklmnopqrstuvwxyz123456";
const out = formatAcpErrorChain(`upstream rejected token=${token}`);
expect(out).toMatch(/upstream rejected/);
expect(out).not.toContain(token);
});
it("redacts secret-shaped tokens that arrive in nested cause messages", () => {
const token = "sk-abcdefghijklmnopqrstuvwxyz123456";
const inner = new Error(`upstream rejected token=${token}`);
const acp = new AcpRuntimeError("ACP_TURN_FAILED", "ACP turn failed", { cause: inner });
const out = formatAcpErrorChain(acp);
expect(out).toMatch(/ACP_TURN_FAILED/);
expect(out).toMatch(/upstream rejected/);
expect(out).not.toContain(token);
});
it("redacts common HTTP, provider, and private-key credentials in ACP error text", () => {
const secrets = [
"Authorization: Basic dXNlcjpwYXNzd29yZGFiY2RlZg==",
"Bearer eyJabcdefghijklmnopqrstuvwxyz.abcdefghijklmnopqrstuvwxyz.abcdefghijklmnopqrstuvwxyz",
"github_pat_abcdefghijklmnopqrstuvwxyz123456",
["xoxb", "1234567890", "abcdefghijklmnop"].join("-"),
"bot123456789:abcdefghijklmnopqrstuvwxyz123456",
"-----BEGIN PRIVATE KEY-----\nabcdefghijklmnopqrstuvwxyz\n-----END PRIVATE KEY-----",
];
const out = formatAcpErrorChain(
new AcpRuntimeError("ACP_TURN_FAILED", `backend failed: ${secrets.join(" ")}`),
);
for (const secret of secrets) {
expect(out).not.toContain(secret);
}
expect(out).toContain("backend failed");
});
it("uses a configured host redactor before rendering ACP error text", () => {
configureAcpErrorRedactor((value) => value.replaceAll("custom-secret", "[CUSTOM]"));
const out = formatAcpErrorChain(new AcpRuntimeError("ACP_TURN_FAILED", "custom-secret"));
expect(out).toContain("[CUSTOM]");
expect(out).not.toContain("custom-secret");
});
});