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.
This commit is contained in:
@@ -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<void>();
|
||||
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(() => {});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user