diff --git a/apps/agent-orchestrator/src/server.test.ts b/apps/agent-orchestrator/src/server.test.ts index 6b8aec5..159714b 100644 --- a/apps/agent-orchestrator/src/server.test.ts +++ b/apps/agent-orchestrator/src/server.test.ts @@ -224,6 +224,42 @@ describe("InvokeServer", () => { await server.close(); }); + + /** + * `buildGraphInput` awaits `sessionStore.get` before the graph ever runs + * (see server.ts) -- a Redis blip there rejects that promise before the + * `this.graph.invoke(...).catch(...)` chain even exists to catch it. Left + * unhandled, that crashes the process, wiping every other in-flight + * invocation and turning their polls into "poll failed: 404" for the + * caller. This asserts the record still resolves to "failed" instead. + */ + it("marks the invocation failed (not an unhandled rejection) when buildGraphInput itself rejects", async () => { + const graph: AgentGraphLike = { invoke: vi.fn(), stream: vi.fn() }; + const failingSessionStore = { + get: vi.fn().mockRejectedValue(new Error("redis: connection reset")), + set: vi.fn(), + }; + const server = new InvokeServer(graph, failingSessionStore as unknown as InMemorySessionStore); + const port = await listenOn(server); + + const postRes = await fetch(`http://127.0.0.1:${port}/invoke`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ request: "do a thing", session_id: "session-1" }), + }); + const { id } = (await postRes.json()) as { id: string }; + + await new Promise((r) => setTimeout(r, 10)); + const res = await fetch(`http://127.0.0.1:${port}/invoke/${id}`); + expect(res.status).toBe(200); + expect((await res.json()) as { status: string; error: string }).toMatchObject({ + status: "failed", + error: "redis: connection reset", + }); + expect(graph.invoke).not.toHaveBeenCalled(); + + await server.close(); + }); }); describe("InvokeServer /invoke event field -> IntegrationRoute dispatch (ADR 0024)", () => { diff --git a/apps/agent-orchestrator/src/server.ts b/apps/agent-orchestrator/src/server.ts index 4a0530b..be95193 100644 --- a/apps/agent-orchestrator/src/server.ts +++ b/apps/agent-orchestrator/src/server.ts @@ -891,6 +891,17 @@ export class InvokeServer { ...(remoteControlUrl ? { remoteControlUrl } : {}), }); }); + }).catch((err: unknown) => { + // `buildGraphInput` itself can reject (e.g. `sessionStore.get` hitting + // Redis) before the graph ever runs. Without this, that rejection is + // unhandled -- Node terminates the process, wiping the in-memory + // `invocations` Map and turning every in-flight poll (not just this + // one) into a 404 the caller reports as "poll failed: 404". + this.invocations.set(id, { + id, + status: "failed", + error: err instanceof Error ? err.message : String(err), + }); }); res.writeHead(202, { "content-type": "application/json", location: `/invoke/${id}` }).end(