From 26578a18c8c3bb43bbc311cd8f87ff3eafba2637 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 13 Mar 2026 23:47:23 +0000 Subject: [PATCH] test: share agent acp turn helpers --- src/commands/agent.acp.test.ts | 200 ++++++++++++++------------------- 1 file changed, 83 insertions(+), 117 deletions(-) diff --git a/src/commands/agent.acp.test.ts b/src/commands/agent.acp.test.ts index ab8c9da8a6e..4ad423dcf18 100644 --- a/src/commands/agent.acp.test.ts +++ b/src/commands/agent.acp.test.ts @@ -171,6 +171,61 @@ function subscribeAssistantEvents() { return { assistantEvents, stop }; } +async function runAcpTurnWithAssistantEvents(chunks: string[]) { + const { assistantEvents, stop } = subscribeAssistantEvents(); + const runTurn = createRunTurnFromTextDeltas(chunks); + + mockAcpManager({ + runTurn: (params: unknown) => runTurn(params), + }); + + try { + await agentCommand({ message: "ping", sessionKey: "agent:codex:acp:test" }, runtime); + } finally { + stop(); + } + + const logLines = vi.mocked(runtime.log).mock.calls.map(([first]) => String(first)); + return { assistantEvents, logLines }; +} + +async function runAcpTurnWithTextDeltas(params: { message?: string; chunks: string[] }) { + const runTurn = createRunTurnFromTextDeltas(params.chunks); + mockAcpManager({ + runTurn: (input: unknown) => runTurn(input), + }); + await agentCommand( + { + message: params.message ?? "ping", + sessionKey: "agent:codex:acp:test", + }, + runtime, + ); + return { runTurn }; +} + +function expectPersistedAcpTranscript(params: { + storePath: string; + userContent: string; + assistantText: string; +}) { + const persistedStore = JSON.parse(fs.readFileSync(params.storePath, "utf-8")) as Record< + string, + { sessionFile?: string } + >; + const sessionFile = persistedStore["agent:codex:acp:test"]?.sessionFile; + const messages = readSessionMessages("acp-session-1", params.storePath, sessionFile); + expect(messages).toHaveLength(2); + expect(messages[0]).toMatchObject({ + role: "user", + content: params.userContent, + }); + expect(messages[1]).toMatchObject({ + role: "assistant", + content: [{ type: "text", text: params.assistantText }], + }); +} + async function runAcpSessionWithPolicyOverrides(params: { acpOverrides: Partial>; resolveSession?: Parameters[0]["resolveSession"]; @@ -209,13 +264,7 @@ describe("agentCommand ACP runtime routing", () => { it("routes ACP sessions through AcpSessionManager instead of embedded agent", async () => { await withAcpSessionEnv(async () => { - const runTurn = createRunTurnFromTextDeltas(["ACP_", "OK"]); - - mockAcpManager({ - runTurn: (params: unknown) => runTurn(params), - }); - - await agentCommand({ message: "ping", sessionKey: "agent:codex:acp:test" }, runtime); + const { runTurn } = await runAcpTurnWithTextDeltas({ chunks: ["ACP_", "OK"] }); expect(runTurn).toHaveBeenCalledWith( expect.objectContaining({ @@ -234,64 +283,32 @@ describe("agentCommand ACP runtime routing", () => { it("persists ACP child session history to the transcript store", async () => { await withAcpSessionEnvInfo(async ({ storePath }) => { - const runTurn = createRunTurnFromTextDeltas(["ACP_", "OK"]); - - mockAcpManager({ - runTurn: (params: unknown) => runTurn(params), - }); - - await agentCommand({ message: "ping", sessionKey: "agent:codex:acp:test" }, runtime); - - const persistedStore = JSON.parse(fs.readFileSync(storePath, "utf-8")) as Record< - string, - { sessionFile?: string } - >; - const sessionFile = persistedStore["agent:codex:acp:test"]?.sessionFile; - const messages = readSessionMessages("acp-session-1", storePath, sessionFile); - expect(messages).toHaveLength(2); - expect(messages[0]).toMatchObject({ - role: "user", - content: "ping", - }); - expect(messages[1]).toMatchObject({ - role: "assistant", - content: [{ type: "text", text: "ACP_OK" }], + await runAcpTurnWithTextDeltas({ chunks: ["ACP_", "OK"] }); + expectPersistedAcpTranscript({ + storePath, + userContent: "ping", + assistantText: "ACP_OK", }); }); }); it("preserves exact ACP transcript text without trimming whitespace", async () => { await withAcpSessionEnvInfo(async ({ storePath }) => { - const runTurn = createRunTurnFromTextDeltas([" ACP_OK\n"]); - - mockAcpManager({ - runTurn: (params: unknown) => runTurn(params), + await runAcpTurnWithTextDeltas({ + message: " ping\n", + chunks: [" ACP_OK\n"], }); - - await agentCommand({ message: " ping\n", sessionKey: "agent:codex:acp:test" }, runtime); - - const persistedStore = JSON.parse(fs.readFileSync(storePath, "utf-8")) as Record< - string, - { sessionFile?: string } - >; - const sessionFile = persistedStore["agent:codex:acp:test"]?.sessionFile; - const messages = readSessionMessages("acp-session-1", storePath, sessionFile); - expect(messages).toHaveLength(2); - expect(messages[0]).toMatchObject({ - role: "user", - content: " ping\n", - }); - expect(messages[1]).toMatchObject({ - role: "assistant", - content: [{ type: "text", text: " ACP_OK\n" }], + expectPersistedAcpTranscript({ + storePath, + userContent: " ping\n", + assistantText: " ACP_OK\n", }); }); }); it("suppresses ACP NO_REPLY lead fragments before emitting assistant text", async () => { await withAcpSessionEnv(async () => { - const { assistantEvents, stop } = subscribeAssistantEvents(); - const runTurn = createRunTurnFromTextDeltas([ + const { assistantEvents, logLines } = await runAcpTurnWithAssistantEvents([ "NO", "NO_", "NO_RE", @@ -299,19 +316,7 @@ describe("agentCommand ACP runtime routing", () => { "Actual answer", ]); - mockAcpManager({ - runTurn: (params: unknown) => runTurn(params), - }); - - try { - await agentCommand({ message: "ping", sessionKey: "agent:codex:acp:test" }, runtime); - } finally { - stop(); - } - expect(assistantEvents).toEqual([{ text: "Actual answer", delta: "Actual answer" }]); - - const logLines = vi.mocked(runtime.log).mock.calls.map(([first]) => String(first)); expect(logLines.some((line) => line.includes("NO_REPLY"))).toBe(false); expect(logLines.some((line) => line.includes("Actual answer"))).toBe(true); }); @@ -319,31 +324,13 @@ describe("agentCommand ACP runtime routing", () => { it("keeps silent-only ACP turns out of assistant output", async () => { await withAcpSessionEnv(async () => { - const assistantEvents: string[] = []; - const stop = onAgentEvent((evt) => { - if (evt.stream !== "assistant") { - return; - } - if (typeof evt.data?.text === "string") { - assistantEvents.push(evt.data.text); - } - }); - - const runTurn = createRunTurnFromTextDeltas(["NO", "NO_", "NO_RE", "NO_REPLY"]); - - mockAcpManager({ - runTurn: (params: unknown) => runTurn(params), - }); - - try { - await agentCommand({ message: "ping", sessionKey: "agent:codex:acp:test" }, runtime); - } finally { - stop(); - } - - expect(assistantEvents).toEqual([]); - - const logLines = vi.mocked(runtime.log).mock.calls.map(([first]) => String(first)); + const { assistantEvents, logLines } = await runAcpTurnWithAssistantEvents([ + "NO", + "NO_", + "NO_RE", + "NO_REPLY", + ]); + expect(assistantEvents.map((event) => event.text).filter(Boolean)).toEqual([]); expect(logLines.some((line) => line.includes("NO_REPLY"))).toBe(false); expect(logLines.some((line) => line.includes("No reply from agent."))).toBe(true); }); @@ -351,18 +338,12 @@ describe("agentCommand ACP runtime routing", () => { it("preserves repeated identical ACP delta chunks", async () => { await withAcpSessionEnv(async () => { - const { assistantEvents, stop } = subscribeAssistantEvents(); - const runTurn = createRunTurnFromTextDeltas(["b", "o", "o", "k"]); - - mockAcpManager({ - runTurn: (params: unknown) => runTurn(params), - }); - - try { - await agentCommand({ message: "ping", sessionKey: "agent:codex:acp:test" }, runtime); - } finally { - stop(); - } + const { assistantEvents, logLines } = await runAcpTurnWithAssistantEvents([ + "b", + "o", + "o", + "k", + ]); expect(assistantEvents).toEqual([ { text: "b", delta: "b" }, @@ -370,30 +351,15 @@ describe("agentCommand ACP runtime routing", () => { { text: "boo", delta: "o" }, { text: "book", delta: "k" }, ]); - - const logLines = vi.mocked(runtime.log).mock.calls.map(([first]) => String(first)); expect(logLines.some((line) => line.includes("book"))).toBe(true); }); }); it("re-emits buffered NO prefix when ACP text becomes visible content", async () => { await withAcpSessionEnv(async () => { - const { assistantEvents, stop } = subscribeAssistantEvents(); - const runTurn = createRunTurnFromTextDeltas(["NO", "W"]); - - mockAcpManager({ - runTurn: (params: unknown) => runTurn(params), - }); - - try { - await agentCommand({ message: "ping", sessionKey: "agent:codex:acp:test" }, runtime); - } finally { - stop(); - } + const { assistantEvents, logLines } = await runAcpTurnWithAssistantEvents(["NO", "W"]); expect(assistantEvents).toEqual([{ text: "NOW", delta: "NOW" }]); - - const logLines = vi.mocked(runtime.log).mock.calls.map(([first]) => String(first)); expect(logLines.some((line) => line.includes("NOW"))).toBe(true); }); });