import type { AgentTool } from "@mariozechner/pi-agent-core"; import { fauxAssistantMessage, fauxToolCall } from "@mariozechner/pi-ai"; import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; import { Type } from "@sinclair/typebox"; import { afterEach, describe, expect, it } from "vitest"; import { createHarness, type Harness } from "./harness.js"; function getUserTexts(harness: Harness): string[] { return harness.session.messages .filter((message) => message.role === "user") .map((message) => { const content = message.content; if (typeof content === "string") { return content; } return content .filter((part): part is { type: "text"; text: string } => part.type === "text") .map((part) => part.text) .join("\n"); }); } function getAssistantTexts(harness: Harness): string[] { return harness.session.messages .filter((message) => message.role === "assistant") .map((message) => message.content .filter((part): part is { type: "text"; text: string } => part.type === "text") .map((part) => part.text) .join("\n"), ); } describe("AgentSession queue characterization", () => { const harnesses: Harness[] = []; afterEach(() => { while (harnesses.length > 0) { harnesses.pop()?.cleanup(); } }); it("dispatches extension commands immediately when prompted while idle", async () => { const commandRuns: string[] = []; const harness = await createHarness({ extensionFactories: [ (pi) => { pi.registerCommand("testcmd", { description: "Test command", handler: async (args) => { commandRuns.push(args); }, }); }, ], }); harnesses.push(harness); await harness.session.prompt("/testcmd hello world"); expect(commandRuns).toEqual(["hello world"]); expect(harness.getPendingResponseCount()).toBe(0); expect(harness.session.messages).toEqual([]); }); it("delivers extension-origin steering messages before the next LLM call", async () => { let extensionApi: ExtensionAPI | undefined; let releaseToolExecution: (() => void) | undefined; const toolStarted = new Promise((resolve) => { releaseToolExecution = resolve; }); const waitTool: AgentTool = { name: "wait", label: "Wait", description: "Wait for the test to release execution", parameters: Type.Object({}), execute: async () => { await toolStarted; return { content: [{ type: "text", text: "released" }], details: {}, }; }, }; const harness = await createHarness({ tools: [waitTool], extensionFactories: [ (pi) => { extensionApi = pi; }, ], }); harnesses.push(harness); harness.setResponses([ fauxAssistantMessage(fauxToolCall("wait", {}), { stopReason: "toolUse" }), (context) => { const sawSteer = context.messages.some( (message) => message.role === "user" && typeof message.content !== "string" && message.content.some((part) => part.type === "text" && part.text === "steer now"), ); return fauxAssistantMessage(sawSteer ? "saw steer" : "missing steer"); }, ]); const sawToolStart = new Promise((resolve) => { const unsubscribe = harness.session.subscribe((event) => { if (event.type === "tool_execution_start" && event.toolName === "wait") { unsubscribe(); resolve(); } }); }); const promptPromise = harness.session.prompt("start"); await sawToolStart; await new Promise((resolve) => setTimeout(resolve, 0)); extensionApi?.sendUserMessage("steer now", { deliverAs: "steer" }); releaseToolExecution?.(); await promptPromise; expect(getUserTexts(harness)).toEqual(["start", "steer now"]); expect(getAssistantTexts(harness)).toContain("saw steer"); }); it("treats queued slash-command follow-ups as raw user text instead of dispatching the command", async () => { let extensionApi: ExtensionAPI | undefined; const commandRuns: string[] = []; let releaseToolExecution: (() => void) | undefined; const toolRelease = new Promise((resolve) => { releaseToolExecution = resolve; }); const waitTool: AgentTool = { name: "wait", label: "Wait", description: "Wait for the test to release execution", parameters: Type.Object({}), execute: async () => { await toolRelease; return { content: [{ type: "text", text: "released" }], details: {}, }; }, }; const harness = await createHarness({ tools: [waitTool], extensionFactories: [ (pi) => { extensionApi = pi; pi.registerCommand("testcmd", { description: "Test command", handler: async (args) => { commandRuns.push(args); }, }); }, ], }); harnesses.push(harness); harness.setResponses([ fauxAssistantMessage(fauxToolCall("wait", {}), { stopReason: "toolUse" }), fauxAssistantMessage("first turn complete"), fauxAssistantMessage("queued follow-up handled by model"), ]); const sawToolStart = new Promise((resolve) => { const unsubscribe = harness.session.subscribe((event) => { if (event.type === "tool_execution_start" && event.toolName === "wait") { unsubscribe(); resolve(); } }); }); const promptPromise = harness.session.prompt("start"); await sawToolStart; await new Promise((resolve) => setTimeout(resolve, 0)); extensionApi?.sendUserMessage("/testcmd queued", { deliverAs: "followUp" }); releaseToolExecution?.(); await promptPromise; expect(commandRuns).toEqual([]); expect(getUserTexts(harness)).toEqual(["start", "/testcmd queued"]); expect(getAssistantTexts(harness)).toContain("queued follow-up handled by model"); }); });