mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-26 06:55:12 +00:00
Summary:
- Preserve AcpRuntimeError detail across the ACP lifecycle boundary.
- Redact non-Error lifecycle failure strings and add gateway/agent propagation coverage.
- Align rebased CLI command-hint formatting with current main.
Verification:
- pnpm check:test-types
- pnpm test src/acp/runtime/errors.test.ts src/agents/command/attempt-execution.error-propagation.test.ts src/gateway/server.agent.gateway-server-agent-b.test.ts
- CI exact head c96d63298b green
Co-authored-by: Jordan Baker <23538+hexsprite@users.noreply.github.com>
95 lines
3.4 KiB
TypeScript
95 lines
3.4 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
import { AcpRuntimeError } from "../../acp/runtime/errors.js";
|
|
import {
|
|
type AgentEventPayload,
|
|
onAgentEvent,
|
|
resetAgentEventsForTest,
|
|
} from "../../infra/agent-events.js";
|
|
import { emitAcpLifecycleError, formatAcpLifecycleError } from "./attempt-execution.js";
|
|
|
|
let captured: AgentEventPayload[] = [];
|
|
let unsubscribe: (() => void) | undefined;
|
|
|
|
beforeEach(() => {
|
|
resetAgentEventsForTest();
|
|
captured = [];
|
|
unsubscribe = onAgentEvent((evt) => {
|
|
captured.push(evt);
|
|
});
|
|
});
|
|
|
|
afterEach(() => {
|
|
unsubscribe?.();
|
|
unsubscribe = undefined;
|
|
resetAgentEventsForTest();
|
|
});
|
|
|
|
describe("emitAcpLifecycleError preserves AcpRuntimeError detail (regression: openclaw-4a8)", () => {
|
|
it("renders the AcpRuntimeError code into the error string so existing consumers surface it", () => {
|
|
const acpError = new AcpRuntimeError("ACP_TURN_FAILED", "ACP turn failed before completion.");
|
|
|
|
emitAcpLifecycleError({ runId: "run-1", error: acpError });
|
|
|
|
expect(captured).toHaveLength(1);
|
|
const data = captured[0]?.data as Record<string, unknown> | undefined;
|
|
expect(data?.phase).toBe("error");
|
|
const text = data?.error as string;
|
|
expect(text).toMatch(/ACP_TURN_FAILED/);
|
|
expect(text).toMatch(/ACP turn failed before completion\./);
|
|
});
|
|
|
|
it("flattens the cause chain into the error string so the underlying RequestError is not lost", () => {
|
|
const rootCause = new Error('RequestError: "Method not found": nes/close (-32601)');
|
|
const wrapped = new Error("Agent does not support session/close (oneshot:abc)", {
|
|
cause: rootCause,
|
|
});
|
|
const acpError = new AcpRuntimeError("ACP_TURN_FAILED", "Internal error", {
|
|
cause: wrapped,
|
|
});
|
|
|
|
emitAcpLifecycleError({ runId: "run-2", error: acpError });
|
|
|
|
const data = captured[0]?.data as Record<string, unknown> | undefined;
|
|
const text = data?.error as string;
|
|
|
|
expect(text).toMatch(/ACP_TURN_FAILED/);
|
|
expect(text).toMatch(/Internal error/);
|
|
expect(text).toMatch(/Agent does not support session\/close/);
|
|
expect(text).toMatch(/Method not found/);
|
|
expect(text).toMatch(/nes\/close/);
|
|
expect(text).toMatch(/-32601/);
|
|
});
|
|
|
|
it("falls back gracefully when given a plain Error without code or cause", () => {
|
|
const plain = new Error("something went wrong");
|
|
|
|
emitAcpLifecycleError({ runId: "run-3", error: plain });
|
|
|
|
const data = captured[0]?.data as Record<string, unknown> | undefined;
|
|
expect(data?.phase).toBe("error");
|
|
expect(data?.error).toBe("Error: something went wrong");
|
|
});
|
|
|
|
it("formats non-Error values without crashing", () => {
|
|
expect(formatAcpLifecycleError("just a string")).toBe("just a string");
|
|
expect(formatAcpLifecycleError(42)).toBe("42");
|
|
expect(formatAcpLifecycleError(undefined)).toBe("undefined");
|
|
|
|
const token = "sk-abcdefghijklmnopqrstuvwxyz123456";
|
|
const text = formatAcpLifecycleError(`upstream rejected token=${token}`);
|
|
expect(text).toMatch(/upstream rejected/);
|
|
expect(text).not.toContain(token);
|
|
});
|
|
|
|
it("caps cause-chain depth so a self-referential cause cannot loop", () => {
|
|
const e: Error & { cause?: unknown } = new Error("loop");
|
|
e.cause = e;
|
|
|
|
const text = formatAcpLifecycleError(e);
|
|
|
|
// Should produce a finite string with the message, not hang.
|
|
expect(text).toMatch(/loop/);
|
|
expect(text.length).toBeLessThan(2000);
|
|
});
|
|
});
|