mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 20:35:39 -06:00
fix(acp): catch unhandled rejection from handleGatewayEvent in event callback (#100558)
* fix(acp): catch unhandled rejection from handleGatewayEvent in event callback * fix(acp): catch unhandled rejection from handleGatewayEvent * test(acp): add regression test for handleGatewayEvent error boundary Verifies that when handleChatEvent throws, handleGatewayEvent catches the error, logs it via this.log(), and does not reject. * chore: remove committed PR body artifact * fix(acp): contain rejected gateway events * chore: defer ACP release note * refactor(acp): simplify gateway rejection boundary --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
type GatewayClientCallbacks = {
|
||||
onEvent?: (evt: { event: string; payload?: unknown }) => void;
|
||||
onHelloOk?: () => void;
|
||||
onConnectError?: (err: Error) => void;
|
||||
onClose?: (code: number, reason: string) => void;
|
||||
@@ -33,6 +34,7 @@ const mockState = vi.hoisted(() => ({
|
||||
gatewayAuth: [] as GatewayClientAuth[],
|
||||
gatewayOptions: [] as GatewayClientOptions[],
|
||||
agentSideConnectionCtor: vi.fn(),
|
||||
agentHandleGatewayEvent: vi.fn(async (_evt: unknown) => {}),
|
||||
agentStart: vi.fn(),
|
||||
routeLogsToStderr: vi.fn(),
|
||||
startProxy: vi.fn(async (_configForTest: unknown) => null as unknown),
|
||||
@@ -70,6 +72,10 @@ class MockGatewayClient {
|
||||
emitConnectError(message: string): void {
|
||||
this.callbacks.onConnectError?.(new Error(message));
|
||||
}
|
||||
|
||||
emitEvent(event: { event: string; payload?: unknown }): void {
|
||||
this.callbacks.onEvent?.(event);
|
||||
}
|
||||
}
|
||||
|
||||
vi.mock("@agentclientprotocol/sdk", () => ({
|
||||
@@ -171,7 +177,9 @@ vi.mock("./translator.js", () => ({
|
||||
|
||||
handleGatewayDisconnect(): void {}
|
||||
|
||||
async handleGatewayEvent(): Promise<void> {}
|
||||
async handleGatewayEvent(event: unknown): Promise<void> {
|
||||
await mockState.agentHandleGatewayEvent(event);
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -283,6 +291,7 @@ describe("serveAcpGateway startup", () => {
|
||||
mockState.gatewayAuth.length = 0;
|
||||
mockState.gatewayOptions.length = 0;
|
||||
mockState.agentSideConnectionCtor.mockReset();
|
||||
mockState.agentHandleGatewayEvent.mockReset();
|
||||
mockState.agentStart.mockReset();
|
||||
mockState.routeLogsToStderr.mockReset();
|
||||
mockState.startProxy.mockReset();
|
||||
@@ -330,6 +339,47 @@ describe("serveAcpGateway startup", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "default logging",
|
||||
opts: {},
|
||||
expected: ["openclaw acp: gateway event chat failed\n"],
|
||||
},
|
||||
{
|
||||
name: "verbose logging",
|
||||
opts: { verbose: true },
|
||||
expected: [
|
||||
"openclaw acp: gateway event chat failed\n",
|
||||
"openclaw acp: gateway event chat error: Error: handler boom\n",
|
||||
],
|
||||
},
|
||||
])("contains rejected gateway event handling with $name", async ({ opts, expected }) => {
|
||||
const { signalHandlers, onceSpy } = captureProcessSignalHandlers();
|
||||
const writes: string[] = [];
|
||||
const writeSpy = vi
|
||||
.spyOn(process.stderr, "write")
|
||||
.mockImplementation((chunk: string | Uint8Array): boolean => {
|
||||
writes.push(String(chunk));
|
||||
return true;
|
||||
});
|
||||
mockState.agentHandleGatewayEvent.mockRejectedValueOnce(new Error("handler boom"));
|
||||
|
||||
try {
|
||||
const servePromise = serveAcpGateway(opts);
|
||||
await emitHelloAndWaitForAgentSideConnection();
|
||||
|
||||
getMockGateway().emitEvent({ event: "chat" });
|
||||
await vi.waitFor(() => {
|
||||
expect(writes).toEqual(expected);
|
||||
});
|
||||
|
||||
await stopServeWithSigint(signalHandlers, servePromise);
|
||||
} finally {
|
||||
writeSpy.mockRestore();
|
||||
onceSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it("routes logs to stderr before loading gateway config", async () => {
|
||||
const { signalHandlers, onceSpy } = captureProcessSignalHandlers();
|
||||
|
||||
|
||||
+8
-1
@@ -91,7 +91,14 @@ export async function serveAcpGateway(opts: AcpServerOptions = {}): Promise<void
|
||||
mode: GATEWAY_CLIENT_MODES.CLI,
|
||||
caps: [GATEWAY_CLIENT_CAPS.TOOL_EVENTS],
|
||||
onEvent: (evt) => {
|
||||
void agent?.handleGatewayEvent(evt);
|
||||
// Gateway delivery stays non-blocking, but translator failures must not
|
||||
// escape this callback as unhandled process rejections.
|
||||
void agent?.handleGatewayEvent(evt).catch((err: unknown) => {
|
||||
process.stderr.write(`openclaw acp: gateway event ${evt.event} failed\n`);
|
||||
if (opts.verbose) {
|
||||
process.stderr.write(`openclaw acp: gateway event ${evt.event} error: ${String(err)}\n`);
|
||||
}
|
||||
});
|
||||
},
|
||||
onHelloOk: () => {
|
||||
resolveGatewayReady();
|
||||
|
||||
Reference in New Issue
Block a user