fix(acp): preserve RequestError details

This commit is contained in:
vyctorbrzezowski
2026-05-12 18:59:32 -03:00
committed by Shakker
parent 207fb9951d
commit c5071a8061
3 changed files with 139 additions and 5 deletions

View File

@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
import { formatAcpRuntimeErrorText } from "./error-text.js";
import { AcpRuntimeError } from "./errors.js";
import { formatAcpRuntimeErrorText, toAcpRuntimeErrorText } from "./error-text.js";
import { AcpRuntimeError, toAcpRuntimeError } from "./errors.js";
describe("formatAcpRuntimeErrorText", () => {
it("adds actionable next steps for known ACP runtime error codes", () => {
@@ -18,4 +18,49 @@ describe("formatAcpRuntimeErrorText", () => {
"ACP error (ACP_TURN_FAILED): turn failed\nnext: Retry, or use `/acp cancel` and send the message again.",
);
});
it("surfaces redacted numeric RequestError details in runtime failure text", () => {
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 text = formatAcpRuntimeErrorText(
toAcpRuntimeError({
error: requestError,
fallbackCode: "ACP_TURN_FAILED",
fallbackMessage: "fallback",
}),
);
expect(text).toContain(
"ACP error (ACP_TURN_FAILED): Internal error: Unknown config option: timeout",
);
expect(text).toContain("next: Retry");
expect(text).not.toContain(token);
});
it("applies the same RequestError details normalization through text conversion", () => {
const requestError = Object.assign(new Error("Internal error"), {
name: "RequestError",
code: -32603,
data: {
details: "Unknown config option: timeout",
},
});
const text = toAcpRuntimeErrorText({
error: requestError,
fallbackCode: "ACP_TURN_FAILED",
fallbackMessage: "fallback",
});
expect(text).toContain(
"ACP error (ACP_TURN_FAILED): Internal error: Unknown config option: timeout",
);
});
});

View File

@@ -3,6 +3,7 @@ import {
AcpRuntimeError,
formatAcpErrorChain,
isAcpRuntimeError,
toAcpRuntimeError,
withAcpRuntimeErrorBoundary,
} from "./errors.js";
@@ -72,6 +73,65 @@ describe("withAcpRuntimeErrorBoundary", () => {
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", () => {

View File

@@ -43,6 +43,31 @@ function getForeignAcpRuntimeError(value: unknown): {
};
}
function readAcpRequestErrorDetails(value: Error): string | undefined {
const code = (value as { code?: unknown }).code;
if (typeof code !== "number") {
return undefined;
}
const data = (value as { data?: unknown }).data;
if (!data || typeof data !== "object") {
return undefined;
}
const details = (data as { details?: unknown }).details;
if (details === undefined || details === null) {
return undefined;
}
const rendered = redactSensitiveText(stringifyNonErrorCause(details)).trim();
return rendered.length > 0 ? rendered : undefined;
}
function messageWithAcpRequestErrorDetails(error: Error): string {
const details = readAcpRequestErrorDetails(error);
if (!details || error.message.includes(details)) {
return error.message;
}
return `${error.message}: ${details}`;
}
export function isAcpRuntimeError(value: unknown): value is AcpRuntimeError {
return value instanceof AcpRuntimeError || getForeignAcpRuntimeError(value) !== null;
}
@@ -62,9 +87,13 @@ export function toAcpRuntimeError(params: {
});
}
if (params.error instanceof Error) {
return new AcpRuntimeError(params.fallbackCode, params.error.message, {
cause: params.error,
});
return new AcpRuntimeError(
params.fallbackCode,
messageWithAcpRequestErrorDetails(params.error),
{
cause: params.error,
},
);
}
return new AcpRuntimeError(params.fallbackCode, params.fallbackMessage, {
cause: params.error,