diff --git a/src/acp/server.startup.test.ts b/src/acp/server.startup.test.ts index 3fbc39d2e269..f7e3f60dfd59 100644 --- a/src/acp/server.startup.test.ts +++ b/src/acp/server.startup.test.ts @@ -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 {} + async handleGatewayEvent(event: unknown): Promise { + 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(); diff --git a/src/acp/server.ts b/src/acp/server.ts index 759641809b9c..f65f3b32454c 100644 --- a/src/acp/server.ts +++ b/src/acp/server.ts @@ -91,7 +91,14 @@ export async function serveAcpGateway(opts: AcpServerOptions = {}): Promise { - 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();