test(agent): cover background wake-up forwarding between turns#3200
Conversation
|
Warning This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
This stack of pull requests is managed by Graphite. Learn more about stacking. |
|
React Doctor found no issues in the changed files. 🎉 Reviewed by React Doctor for commit |
|
Reviews (1): Last reviewed commit: "add wake-up forwarding regression tests" | Re-trigger Greptile |
|
|
||
| interface ClientMocks { | ||
| sessionUpdate: ReturnType<typeof vi.fn>; | ||
| extNotification: ReturnType<typeof vi.fn>; | ||
| } | ||
|
|
||
| function makeAgent(): { agent: Agent; client: ClientMocks } { | ||
| const client: ClientMocks = { | ||
| sessionUpdate: vi.fn().mockResolvedValue(undefined), | ||
| extNotification: vi.fn().mockResolvedValue(undefined), | ||
| }; | ||
| const agent = new ClaudeAcpAgent(client as unknown as AgentSideConnection); | ||
| return { agent, client }; | ||
| } | ||
|
|
||
| function installFakeSession( | ||
| agent: Agent, | ||
| sessionId: string, | ||
| ): { query: MockQuery; input: Pushable<SDKUserMessage> } { | ||
| const query = createMockQuery(); | ||
| const input = new Pushable<SDKUserMessage>(); | ||
| const abortController = new AbortController(); | ||
|
|
||
| const session = { | ||
| query, | ||
| queryOptions: { sessionId, cwd: "/tmp/repo", abortController }, | ||
| buildInProcessMcpServers: () => ({}), | ||
| localToolsServerNames: [] as string[], | ||
| input, | ||
| cancelled: false, | ||
| interruptReason: undefined, | ||
| settingsManager: { dispose: vi.fn(), getRepoRoot: () => "/tmp/repo" }, | ||
| permissionMode: "default" as const, | ||
| abortController, | ||
| accumulatedUsage: { | ||
| inputTokens: 0, | ||
| outputTokens: 0, | ||
| cachedReadTokens: 0, | ||
| cachedWriteTokens: 0, | ||
| }, | ||
| sessionResources: new Set(), | ||
| configOptions: [], | ||
| turnQueue: [], | ||
| activeTurn: null, | ||
| pendingOrphanResults: 0, | ||
| queryGeneration: 0, | ||
| cwd: "/tmp/repo", | ||
| notificationHistory: [] as unknown[], | ||
| taskRunId: "run-1", | ||
| lastContextWindowSize: 200_000, | ||
| modelId: "claude-sonnet-4-6", | ||
| taskState: new Map(), | ||
| }; | ||
|
|
||
| (agent as unknown as { session: typeof session }).session = session; | ||
| (agent as unknown as { sessionId: string }).sessionId = sessionId; | ||
|
|
||
| return { query, input }; | ||
| } | ||
|
|
||
| function tick(): Promise<void> { | ||
| return new Promise((resolve) => setImmediate(resolve)); | ||
| } | ||
|
|
||
| async function send(query: MockQuery, message: unknown): Promise<void> { | ||
| query._mockHelpers.sendMessage(message as SDKMessage); | ||
| await tick(); | ||
| } | ||
|
|
||
| // Replays the prompt's own user message back through the query so the consumer | ||
| // activates its Turn before the terminal result arrives. | ||
| async function echoUserMessage( | ||
| query: MockQuery, | ||
| input: Pushable<SDKUserMessage>, | ||
| ): Promise<void> { | ||
| const { value: pushed } = await input[Symbol.asyncIterator]().next(); | ||
| await send(query, pushed); | ||
| } | ||
|
|
||
| function messageStart(sessionId: string, apiId: string) { | ||
| return { | ||
| type: "stream_event", | ||
| parent_tool_use_id: null, | ||
| session_id: sessionId, | ||
| uuid: `start-${apiId}`, | ||
| event: { type: "message_start", message: { id: apiId, usage: {} } }, | ||
| }; | ||
| } | ||
|
|
||
| function textDelta(sessionId: string, text: string) { | ||
| return { | ||
| type: "stream_event", | ||
| parent_tool_use_id: null, | ||
| session_id: sessionId, | ||
| uuid: `delta-${text}`, | ||
| event: { | ||
| type: "content_block_delta", | ||
| index: 0, | ||
| delta: { type: "text_delta", text }, | ||
| }, | ||
| }; | ||
| } | ||
|
|
||
| function assistantMessage(sessionId: string, apiId: string, text: string) { | ||
| return { | ||
| type: "assistant", | ||
| parent_tool_use_id: null, | ||
| session_id: sessionId, | ||
| uuid: `assistant-${apiId}`, | ||
| message: { | ||
| id: apiId, | ||
| role: "assistant", | ||
| content: [{ type: "text", text }], | ||
| }, | ||
| }; | ||
| } | ||
|
|
||
| function resultSuccess(sessionId: string, uuid: string) { | ||
| return { | ||
| type: "result", | ||
| subtype: "success", | ||
| session_id: sessionId, | ||
| uuid, | ||
| result: "", | ||
| is_error: false, | ||
| usage: {}, | ||
| modelUsage: {}, | ||
| }; | ||
| } | ||
|
|
||
| function taskNotificationResult(sessionId: string, uuid: string) { | ||
| return { | ||
| ...resultSuccess(sessionId, uuid), | ||
| origin: { kind: "task-notification" }, | ||
| }; | ||
| } | ||
|
|
||
| function taskNotification(sessionId: string) { | ||
| return { | ||
| type: "system", | ||
| subtype: "task_notification", | ||
| session_id: sessionId, | ||
| uuid: "task-note-1", | ||
| task_id: "task-1", | ||
| status: "completed", | ||
| output_file: "/tmp/task-1.out", | ||
| summary: "Background command completed", | ||
| }; | ||
| } | ||
|
|
||
| function messageChunkTexts( | ||
| calls: ClientMocks["sessionUpdate"]["mock"]["calls"], | ||
| ): string[] { | ||
| return calls | ||
| .map( | ||
| ([call]) => | ||
| ( | ||
| call as { | ||
| update?: { sessionUpdate?: string; content?: { text?: string } }; | ||
| } | ||
| ).update, | ||
| ) | ||
| .filter((update) => update?.sessionUpdate === "agent_message_chunk") | ||
| .map((update) => update?.content?.text ?? ""); | ||
| } | ||
|
|
||
| async function runTurn( | ||
| agent: Agent, | ||
| client: ClientMocks, | ||
| query: MockQuery, | ||
| input: Pushable<SDKUserMessage>, | ||
| sessionId: string, | ||
| text: string, |
There was a problem hiding this comment.
Verbatim duplication of shared test helpers
ClientMocks, makeAgent, installFakeSession, tick, send, echoUserMessage, messageStart, textDelta, assistantMessage, and messageChunkTexts are copied character-for-character from claude-agent.streamed-text.test.ts. A third test file for this adapter will copy them again. Extracting these into a shared module (e.g., claude-agent.test-helpers.ts) would satisfy the OnceAndOnlyOnce rule and keep all adapter test files in sync automatically.
Context Used: Do not attempt to comment on incorrect alphabetica... (source)
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
8f12c5e
into
chore/sync-claude-adapter-v0.54.1

Problem
Agent wake-ups (Bash
run_in_backgroundtask notifications, Monitor andScheduleWakeuptimers) fire after the turn has settled. Onmainthe SDK stream is only read insideprompt(), so wake-up output sits unread until the next user message and then appears all at once. The session looks hung for anyone waiting on a background task or a scheduled wake-up.Closes #1919
Closes #2414
Changes
The fix itself lands in #3076 below this PR: its upstream #780 port replaces the per-prompt read loop with a session-lifetime consumer that forwards between-turn output live. This PR stacks regression tests on top so that behavior can't silently regress:
How did you test this?
pnpm --filter @posthog/agent test: 931 tests pass, including the 4 new onessleep 10 && echo finishedin the background, turn ended withend_turn, "the test succeeded" streamed in live 9s later with no follow-up promptScheduleWakeUptool call properly #2414: agent calledScheduleWakeupfor 60s out, turn ended withend_turn, "the wakeup succeeded" streamed in live when the timer fired with no follow-up promptAutomatic notifications