From 030e950e5fa623741d65d4846f560b186c55955b Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 22 Mar 2026 15:10:21 -0700 Subject: [PATCH] test: simplify ACP spawn scenarios --- src/agents/acp-spawn.test.ts | 192 ++++++++++++++++++++--------------- 1 file changed, 110 insertions(+), 82 deletions(-) diff --git a/src/agents/acp-spawn.test.ts b/src/agents/acp-spawn.test.ts index 58cef62b5b4..d14cb74ae8a 100644 --- a/src/agents/acp-spawn.test.ts +++ b/src/agents/acp-spawn.test.ts @@ -92,6 +92,14 @@ const resolveAcpSpawnStreamLogPathSpy = vi.spyOn( ); const { spawnAcpDirect } = await import("./acp-spawn.js"); +type SpawnRequest = Parameters[0]; +type SpawnContext = Parameters[1]; +type AgentCallParams = { + deliver?: boolean; + channel?: string; + to?: string; + threadId?: string; +}; function replaceSpawnConfig(next: OpenClawConfig): void { const current = hoisted.state.cfg as Record; @@ -153,6 +161,64 @@ function expectResolvedIntroTextInBindMetadata(): void { expect(introText.includes("session ids: pending (available after the first reply)")).toBe(false); } +function createSpawnRequest(overrides?: Partial): SpawnRequest { + return { + task: "Investigate flaky tests", + agentId: "codex", + mode: "run", + ...overrides, + }; +} + +function createRequesterContext(overrides?: Partial): SpawnContext { + return { + agentSessionKey: "agent:main:telegram:direct:6098642967", + agentChannel: "telegram", + agentAccountId: "default", + agentTo: "telegram:6098642967", + agentThreadId: "1", + ...overrides, + }; +} + +function findAgentGatewayCall(): { method?: string; params?: Record } | undefined { + return hoisted.callGatewayMock.mock.calls + .map((call: unknown[]) => call[0] as { method?: string; params?: Record }) + .find((request) => request.method === "agent"); +} + +function expectAgentGatewayCall(overrides: AgentCallParams): void { + const agentCall = findAgentGatewayCall(); + expect(agentCall?.params?.deliver).toBe(overrides.deliver); + expect(agentCall?.params?.channel).toBe(overrides.channel); + expect(agentCall?.params?.to).toBe(overrides.to); + expect(agentCall?.params?.threadId).toBe(overrides.threadId); +} + +function enableMatrixAcpThreadBindings(): void { + replaceSpawnConfig({ + ...hoisted.state.cfg, + channels: { + ...hoisted.state.cfg.channels, + matrix: { + threadBindings: { + enabled: true, + spawnAcpSessions: true, + }, + }, + }, + }); + registerSessionBindingAdapter({ + channel: "matrix", + accountId: "default", + capabilities: createSessionBindingCapabilities(), + bind: async (input) => await hoisted.sessionBindingBindMock(input), + listBySession: (targetSessionKey) => hoisted.sessionBindingListBySessionMock(targetSessionKey), + resolveByConversation: (ref) => hoisted.sessionBindingResolveByConversationMock(ref), + unbind: async (input) => await hoisted.sessionBindingUnbindMock(input), + }); +} + describe("spawnAcpDirect", () => { beforeEach(() => { replaceSpawnConfig(createDefaultSpawnConfig()); @@ -383,28 +449,7 @@ describe("spawnAcpDirect", () => { }); it("spawns Matrix thread-bound ACP sessions from top-level room targets", async () => { - replaceSpawnConfig({ - ...hoisted.state.cfg, - channels: { - ...hoisted.state.cfg.channels, - matrix: { - threadBindings: { - enabled: true, - spawnAcpSessions: true, - }, - }, - }, - }); - registerSessionBindingAdapter({ - channel: "matrix", - accountId: "default", - capabilities: createSessionBindingCapabilities(), - bind: async (input) => await hoisted.sessionBindingBindMock(input), - listBySession: (targetSessionKey) => - hoisted.sessionBindingListBySessionMock(targetSessionKey), - resolveByConversation: (ref) => hoisted.sessionBindingResolveByConversationMock(ref), - unbind: async (input) => await hoisted.sessionBindingUnbindMock(input), - }); + enableMatrixAcpThreadBindings(); hoisted.sessionBindingBindMock.mockImplementationOnce( async (input: { targetSessionKey: string; @@ -453,74 +498,57 @@ describe("spawnAcpDirect", () => { }), }), ); - const agentCall = hoisted.callGatewayMock.mock.calls - .map((call: unknown[]) => call[0] as { method?: string; params?: Record }) - .find((request) => request.method === "agent"); - expect(agentCall?.params?.channel).toBe("matrix"); - expect(agentCall?.params?.to).toBe("room:!room:example"); - expect(agentCall?.params?.threadId).toBe("child-thread"); + expectAgentGatewayCall({ + deliver: true, + channel: "matrix", + to: "room:!room:example", + threadId: "child-thread", + }); }); - it("inlines delivery for run-mode spawns from non-subagent requester sessions", async () => { - const result = await spawnAcpDirect( - { - task: "Investigate flaky tests", - agentId: "codex", - mode: "run", - }, - { - agentSessionKey: "agent:main:telegram:direct:6098642967", - agentChannel: "telegram", - agentAccountId: "default", - agentTo: "telegram:6098642967", - agentThreadId: "1", - }, - ); - - expect(result.status).toBe("accepted"); - expect(result.mode).toBe("run"); - expect(result.streamLogPath).toBeUndefined(); - expect(hoisted.startAcpSpawnParentStreamRelayMock).not.toHaveBeenCalled(); - expect(hoisted.resolveSessionTranscriptFileMock).toHaveBeenCalledWith( - expect.objectContaining({ - sessionId: "sess-123", - storePath: "/tmp/codex-sessions.json", - agentId: "codex", - }), - ); - const agentCall = hoisted.callGatewayMock.mock.calls - .map((call: unknown[]) => call[0] as { method?: string; params?: Record }) - .find((request) => request.method === "agent"); - expect(agentCall?.params?.deliver).toBe(true); - expect(agentCall?.params?.channel).toBe("telegram"); - expect(agentCall?.params?.to).toBe("telegram:6098642967"); - }); - - it("does not inline delivery for run-mode spawns from subagent requester sessions", async () => { - const result = await spawnAcpDirect( - { - task: "Investigate flaky tests", - agentId: "codex", - mode: "run", - }, - { + it.each([ + { + name: "inlines delivery for run-mode spawns from non-subagent requester sessions", + ctx: createRequesterContext(), + expectedAgentCall: { + deliver: true, + channel: "telegram", + to: "telegram:6098642967", + threadId: "1", + } satisfies AgentCallParams, + expectTranscriptPersistence: true, + }, + { + name: "does not inline delivery for run-mode spawns from subagent requester sessions", + ctx: createRequesterContext({ agentSessionKey: "agent:main:subagent:orchestrator", - agentChannel: "telegram", - agentAccountId: "default", - agentTo: "telegram:6098642967", - }, - ); + agentThreadId: undefined, + }), + expectedAgentCall: { + deliver: false, + channel: undefined, + to: undefined, + threadId: undefined, + } satisfies AgentCallParams, + expectTranscriptPersistence: false, + }, + ])("$name", async ({ ctx, expectedAgentCall, expectTranscriptPersistence }) => { + const result = await spawnAcpDirect(createSpawnRequest(), ctx); expect(result.status).toBe("accepted"); expect(result.mode).toBe("run"); expect(result.streamLogPath).toBeUndefined(); expect(hoisted.startAcpSpawnParentStreamRelayMock).not.toHaveBeenCalled(); - const agentCall = hoisted.callGatewayMock.mock.calls - .map((call: unknown[]) => call[0] as { method?: string; params?: Record }) - .find((request) => request.method === "agent"); - expect(agentCall?.params?.deliver).toBe(false); - expect(agentCall?.params?.channel).toBeUndefined(); - expect(agentCall?.params?.to).toBeUndefined(); + if (expectTranscriptPersistence) { + expect(hoisted.resolveSessionTranscriptFileMock).toHaveBeenCalledWith( + expect.objectContaining({ + sessionId: "sess-123", + storePath: "/tmp/codex-sessions.json", + agentId: "codex", + }), + ); + } + expectAgentGatewayCall(expectedAgentCall); }); it("keeps ACP spawn running when session-file persistence fails", async () => {