From cdca5cf93d38cc67b798e81b8b02bf033f243b21 Mon Sep 17 00:00:00 2001 From: can1357 Date: Wed, 17 Jun 2026 00:52:36 +0200 Subject: [PATCH] test(coding-agent): updated steer idle drain tests for queued streaming behavior - Removed the obsolete non-resumable idle-steer test and updated comments to reflect immediate continue scheduling for idle steering. - Added a real-timer streaming scenario with a mock model to verify queued image steering is surfaced through clearQueue as normalized text/images while the session is streaming. --- .../agent-session-steer-idle-drain.test.ts | 67 +++++++++++++------ 1 file changed, 46 insertions(+), 21 deletions(-) diff --git a/packages/coding-agent/test/agent-session-steer-idle-drain.test.ts b/packages/coding-agent/test/agent-session-steer-idle-drain.test.ts index 4ba96c802..827c644f9 100644 --- a/packages/coding-agent/test/agent-session-steer-idle-drain.test.ts +++ b/packages/coding-agent/test/agent-session-steer-idle-drain.test.ts @@ -2,6 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "bun:test"; import * as path from "node:path"; import { Agent } from "@oh-my-pi/pi-agent-core"; import type { AssistantMessage, ToolResultMessage } from "@oh-my-pi/pi-ai"; +import { createMockModel } from "@oh-my-pi/pi-ai/providers/mock"; import { getBundledModel } from "@oh-my-pi/pi-catalog/models"; import { ModelRegistry } from "@oh-my-pi/pi-coding-agent/config/model-registry"; import { Settings } from "@oh-my-pi/pi-coding-agent/config/settings"; @@ -17,9 +18,11 @@ import { TempDir } from "@oh-my-pi/pi-utils"; * idle drain: the message stranded in the queue (visible chip, never delivered) * until the next manual prompt. * - * Contract: steering an idle, resumable (assistant-ended) session schedules an - * immediate `agent.continue()`; a non-resumable state leaves the message queued - * without starting a turn. + * Contract: steering an idle session schedules an immediate `agent.continue()`, + * so a queued steer is delivered without waiting for the next manual prompt. A + * queued steer resumes from any tail (continue() injects it before the next + * provider call), so there is no "non-resumable steer" case. While a turn is + * still streaming the drain stands down and the steer simply stays queued. */ function createAssistantMessage(): AssistantMessage { @@ -103,21 +106,6 @@ describe("AgentSession steer idle drain", () => { expect(continueSpy).toHaveBeenCalledTimes(1); }); - it("leaves the steer queued when the session is not resumable", async () => { - // Last message is a user message: continue() from this state would run an - // extra model call on the stale prompt, so the drain must not fire. - await createSession([{ role: "user", content: "hello", timestamp: Date.now() }]); - const continueSpy = vi.spyOn(session.agent, "continue").mockResolvedValue(); - - await session.steer("wait for the run"); - - vi.advanceTimersByTime(200); - await session.waitForIdle(); - expect(continueSpy).not.toHaveBeenCalled(); - expect(session.agent.hasQueuedMessages()).toBe(true); - expect(session.getQueuedMessages().steering).toContain("wait for the run"); - }); - it("delivers a steer queued after an interrupted tool result", async () => { await createSession([ { role: "user", content: "hello", timestamp: Date.now() }, @@ -136,14 +124,51 @@ describe("AgentSession steer idle drain", () => { }); it("round-trips queued images through clearQueue for editor restoration", async () => { - // Non-resumable state so the idle drain stays out of the way. - await createSession([{ role: "user", content: "hello", timestamp: Date.now() }]); - const image = { type: "image" as const, data: "abc", mimeType: "image/png" }; + // A steer queued mid-stream stays in the queue (the idle drain stands down while + // streaming), so clearQueue round-trips session.steer's normalized image payload + // for editor restoration. A parked model turn gives a deterministic streaming + // state. Real timers here: the prompt/stream path awaits real timers that the + // suite's fake clock would gate (it hangs otherwise), and the parked turn is + // cancelled by abort via the AbortSignal — never waited on — so there is no 60s wait. + vi.useRealTimers(); + const model = getBundledModel("anthropic", "claude-sonnet-4-5"); + if (!model) throw new Error("Expected built-in anthropic model to exist"); + const started = Promise.withResolvers(); + const mock = createMockModel({ + responses: [ + () => { + started.resolve(); + return { content: ["working"], delayMs: 60_000 }; + }, + ], + }); + const agent = new Agent({ + getApiKey: () => "test-key", + initialState: { model, systemPrompt: ["Test"], tools: [] }, + streamFn: mock.stream, + }); + const sessionManager = SessionManager.create(tempDir.path(), tempDir.path()); + session = new AgentSession({ + agent, + sessionManager, + settings: Settings.isolated({ "compaction.enabled": false }), + modelRegistry: new ModelRegistry(authStorage), + }); + const running = session.prompt("do the thing"); + await started.promise; + expect(session.isStreaming).toBe(true); + + const image = { type: "image" as const, data: "abc", mimeType: "image/png" }; await session.steer("with image", [image]); const { steering } = session.clearQueue(); expect(steering).toEqual([{ text: "with image", images: [image] }]); expect(session.agent.hasQueuedMessages()).toBe(false); + + await session.abort(); + await session.waitForIdle(); + await running.catch(() => {}); }); + });