/** * Tests for task-subagent integration: TaskExecute tool, completion listener, * auto-cascade, and widget agent ID display. */ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import initExtension from "../src/index.js"; import { TaskStore } from "../src/task-store.js"; import { TaskWidget, type Theme, type UICtx } from "../src/ui/task-widget.js"; // Force in-memory task store for all integration tests — prevents file-backed // store from loading stale tasks across test instances. beforeEach(() => { process.env.PI_TASKS = "off"; }); afterEach(() => { delete process.env.PI_TASKS; }); // ---- Mock pi ---- type MockEventBus = { on: (channel: string, handler: (data: unknown) => void) => () => void; emit: (channel: string, data: unknown) => void; }; /** Minimal mock of ExtensionAPI with events, tool capture, and event hooks. */ function mockPi() { const tools = new Map(); const commands = new Map(); const eventHandlers = new Map void)[]>(); const lifecycleHandlers = new Map any)[]>(); const pi = { registerTool(def: any) { tools.set(def.name, def); }, registerCommand(name: string, def: any) { commands.set(name, def); }, on(event: string, handler: any) { if (!lifecycleHandlers.has(event)) lifecycleHandlers.set(event, []); lifecycleHandlers.get(event)!.push(handler); }, events: { emit(channel: string, data: unknown) { for (const h of eventHandlers.get(channel) ?? []) h(data); }, on(channel: string, handler: (data: unknown) => void) { if (!eventHandlers.has(channel)) eventHandlers.set(channel, []); eventHandlers.get(channel)!.push(handler); return () => { const arr = eventHandlers.get(channel); if (arr) eventHandlers.set(channel, arr.filter(h => h !== handler)); }; }, }, sendUserMessage: vi.fn(), }; return { pi, tools, commands, /** Execute a registered tool by name. */ async executeTool(name: string, params: any, ctx?: any) { const tool = tools.get(name); if (!tool) throw new Error(`Tool ${name} not registered`); return tool.execute("call-1", params, undefined, undefined, ctx ?? mockCtx()); }, /** Fire lifecycle event handlers (turn_start, tool_result, etc.) */ async fireLifecycle(event: string, ...args: any[]) { for (const h of lifecycleHandlers.get(event) ?? []) { await h(...args); } }, /** Emit an event on pi.events (simulates subagent extension). */ emitEvent(channel: string, data: unknown) { pi.events.emit(channel, data); }, }; } /** Minimal mock ExtensionContext. */ function mockCtx() { return { model: { id: "test-model", name: "Test" }, modelRegistry: {}, ui: { setWidget: vi.fn(), setStatus: vi.fn(), notify: vi.fn(), }, }; } // ---- Mock subagents extension (RPC responders) ---- /** Simulates the @tintinweb/pi-subagents extension: responds to ping + spawn RPCs and emits ready. */ function installSubagentsMock(pi: { events: MockEventBus }, opts?: { spawnError?: string }) { let idCounter = 0; const spawned: Array<{ id: string; type: string; prompt: string; options: any }> = []; const stopped: string[] = []; // Respond to ping — reply on scoped channel const unsubPing = pi.events.on("subagents:rpc:ping", (data: unknown) => { const { requestId } = data as { requestId: string }; pi.events.emit(`subagents:rpc:ping:reply:${requestId}`, { success: true, data: { version: 2 } }); }); // Respond to spawn — reply on scoped channel const unsubSpawn = pi.events.on("subagents:rpc:spawn", (data: unknown) => { const { requestId, type, prompt, options } = data as { requestId: string; type: string; prompt: string; options?: any; }; if (opts?.spawnError) { pi.events.emit(`subagents:rpc:spawn:reply:${requestId}`, { success: false, error: opts.spawnError }); return; } const id = `agent-${++idCounter}`; spawned.push({ id, type, prompt, options }); pi.events.emit(`subagents:rpc:spawn:reply:${requestId}`, { success: true, data: { id } }); }); // Respond to stop — reply on scoped channel const unsubStop = pi.events.on("subagents:rpc:stop", (data: unknown) => { const { requestId, agentId } = data as { requestId: string; agentId: string }; const known = spawned.some(s => s.id === agentId); if (known) { stopped.push(agentId); pi.events.emit(`subagents:rpc:stop:reply:${requestId}`, { success: true }); } else { pi.events.emit(`subagents:rpc:stop:reply:${requestId}`, { success: false, error: "Agent not found" }); } }); // Broadcast readiness pi.events.emit("subagents:ready", {}); return { spawned, stopped, unsub() { unsubPing(); unsubSpawn(); unsubStop(); }, }; } // ---- Tests ---- describe("TaskExecute", () => { let mock: ReturnType; let rpc: ReturnType; beforeEach(() => { mock = mockPi(); // Install mock BEFORE init so ping reply is received during extension init rpc = installSubagentsMock(mock.pi); initExtension(mock.pi as any); }); afterEach(() => { rpc.unsub(); }); it("is registered as a tool", () => { expect(mock.tools.has("TaskExecute")).toBe(true); }); it("returns error when subagent extension is not loaded", async () => { // Re-init without mock to simulate missing extension const freshMock = mockPi(); initExtension(freshMock.pi as any); await freshMock.executeTool("TaskCreate", { subject: "Test task", description: "Do something", agentType: "general-purpose", }); const result = await freshMock.executeTool("TaskExecute", { task_ids: ["1"] }); expect(result.content[0].text).toContain("Subagent execution is currently unavailable"); }); it("rejects non-existent tasks", async () => { const result = await mock.executeTool("TaskExecute", { task_ids: ["999"] }); expect(result.content[0].text).toContain("#999: not found"); }); it("rejects tasks without agentType", async () => { await mock.executeTool("TaskCreate", { subject: "No agent type", description: "Plain task", }); const result = await mock.executeTool("TaskExecute", { task_ids: ["1"] }); expect(result.content[0].text).toContain("#1: no agentType set"); }); it("rejects non-pending tasks", async () => { await mock.executeTool("TaskCreate", { subject: "Already started", description: "Desc", agentType: "general-purpose", }); await mock.executeTool("TaskUpdate", { taskId: "1", status: "in_progress" }); const result = await mock.executeTool("TaskExecute", { task_ids: ["1"] }); expect(result.content[0].text).toContain("#1: not pending"); }); it("rejects tasks with unresolved blockers", async () => { await mock.executeTool("TaskCreate", { subject: "Blocker", description: "Desc", agentType: "general-purpose", }); await mock.executeTool("TaskCreate", { subject: "Blocked", description: "Desc", agentType: "general-purpose", }); await mock.executeTool("TaskUpdate", { taskId: "2", addBlockedBy: ["1"] }); const result = await mock.executeTool("TaskExecute", { task_ids: ["2"] }); expect(result.content[0].text).toContain("#2: blocked by #1"); }); it("spawns agent for valid task and updates metadata", async () => { await mock.executeTool("TaskCreate", { subject: "Run tests", description: "Run the test suite", agentType: "general-purpose", }); const result = await mock.executeTool("TaskExecute", { task_ids: ["1"] }); expect(result.content[0].text).toContain("Launched 1 agent"); expect(result.content[0].text).toContain("#1 → agent agent-1"); // Verify the RPC responder was called expect(rpc.spawned).toHaveLength(1); expect(rpc.spawned[0].type).toBe("general-purpose"); expect(rpc.spawned[0].prompt).toContain("Run the test suite"); expect(rpc.spawned[0].options.isBackground).toBe(true); }); it("passes additional_context and max_turns to spawned agents", async () => { await mock.executeTool("TaskCreate", { subject: "Explore codebase", description: "Find all API endpoints", agentType: "Explore", }); await mock.executeTool("TaskExecute", { task_ids: ["1"], additional_context: "Focus on REST endpoints only", max_turns: 10, }); expect(rpc.spawned[0].prompt).toContain("Focus on REST endpoints only"); expect(rpc.spawned[0].options.maxTurns).toBe(10); }); it("allows executing tasks whose blockers are all completed", async () => { await mock.executeTool("TaskCreate", { subject: "Blocker", description: "Desc", agentType: "general-purpose", }); await mock.executeTool("TaskCreate", { subject: "Dependent", description: "Desc", agentType: "general-purpose", }); await mock.executeTool("TaskUpdate", { taskId: "2", addBlockedBy: ["1"] }); await mock.executeTool("TaskUpdate", { taskId: "1", status: "completed" }); const result = await mock.executeTool("TaskExecute", { task_ids: ["2"] }); expect(result.content[0].text).toContain("Launched 1 agent"); }); it("handles mixed valid and invalid tasks in one call", async () => { await mock.executeTool("TaskCreate", { subject: "Valid", description: "Desc", agentType: "general-purpose", }); await mock.executeTool("TaskCreate", { subject: "No agent type", description: "Desc", }); const result = await mock.executeTool("TaskExecute", { task_ids: ["1", "2", "999"] }); const text = result.content[0].text; expect(text).toContain("Launched 1 agent"); expect(text).toContain("#2: no agentType set"); expect(text).toContain("#999: not found"); }); }); describe("TaskExecute via ready broadcast", () => { it("detects subagents when ready fires after tasks init", async () => { // Init tasks WITHOUT the mock — subagents not available yet const mock = mockPi(); initExtension(mock.pi as any); // Now install the mock (simulates subagents loading later) and broadcast ready const rpc = installSubagentsMock(mock.pi); // Create a task and execute — should work because ready was received await mock.executeTool("TaskCreate", { subject: "Late-loaded test", description: "Desc", agentType: "general-purpose", }); const result = await mock.executeTool("TaskExecute", { task_ids: ["1"] }); expect(result.content[0].text).toContain("Launched 1 agent"); expect(rpc.spawned).toHaveLength(1); rpc.unsub(); }); }); describe("Completion listener", () => { let mock: ReturnType; let rpc: ReturnType; beforeEach(() => { mock = mockPi(); rpc = installSubagentsMock(mock.pi); initExtension(mock.pi as any); }); afterEach(() => { rpc.unsub(); }); it("marks task completed on subagents:completed event", async () => { await mock.executeTool("TaskCreate", { subject: "Agent task", description: "Desc", agentType: "general-purpose", }); await mock.executeTool("TaskExecute", { task_ids: ["1"] }); // Simulate agent completion mock.emitEvent("subagents:completed", { id: "agent-1" }); const result = await mock.executeTool("TaskGet", { taskId: "1" }); expect(result.content[0].text).toContain("Status: completed"); }); it("reverts task to pending on subagents:failed event", async () => { await mock.executeTool("TaskCreate", { subject: "Failing task", description: "Desc", agentType: "general-purpose", }); await mock.executeTool("TaskExecute", { task_ids: ["1"] }); // Simulate agent failure mock.emitEvent("subagents:failed", { id: "agent-1", error: "Out of turns", status: "error" }); const result = await mock.executeTool("TaskGet", { taskId: "1" }); expect(result.content[0].text).toContain("Status: pending"); }); it("ignores events for unknown agent IDs", async () => { await mock.executeTool("TaskCreate", { subject: "Unrelated", description: "Desc", }); // Should not throw or modify anything mock.emitEvent("subagents:completed", { id: "unknown-agent" }); mock.emitEvent("subagents:failed", { id: "unknown-agent", error: "boom", status: "error" }); const result = await mock.executeTool("TaskGet", { taskId: "1" }); expect(result.content[0].text).toContain("Status: pending"); }); }); describe("Auto-cascade", () => { let mock: ReturnType; let rpc: ReturnType; beforeEach(() => { mock = mockPi(); rpc = installSubagentsMock(mock.pi); initExtension(mock.pi as any); }); afterEach(() => { rpc.unsub(); }); it("does NOT cascade when auto-cascade is off (default)", async () => { // Create A → B chain await mock.executeTool("TaskCreate", { subject: "Task A", description: "Desc", agentType: "general-purpose", }); await mock.executeTool("TaskCreate", { subject: "Task B", description: "Desc", agentType: "general-purpose", }); await mock.executeTool("TaskUpdate", { taskId: "2", addBlockedBy: ["1"] }); // Execute A await mock.executeTool("TaskExecute", { task_ids: ["1"] }); expect(rpc.spawned).toHaveLength(1); // Complete A mock.emitEvent("subagents:completed", { id: "agent-1" }); // B should NOT have been auto-started expect(rpc.spawned).toHaveLength(1); // B should still be pending const result = await mock.executeTool("TaskGet", { taskId: "2" }); expect(result.content[0].text).toContain("Status: pending"); }); it("does NOT cascade on failure (branch stops)", async () => { await mock.executeTool("TaskCreate", { subject: "Task A", description: "Desc", agentType: "general-purpose", }); await mock.executeTool("TaskCreate", { subject: "Task B", description: "Desc", agentType: "general-purpose", }); await mock.executeTool("TaskUpdate", { taskId: "2", addBlockedBy: ["1"] }); await mock.executeTool("TaskExecute", { task_ids: ["1"] }); mock.emitEvent("subagents:failed", { id: "agent-1", error: "crashed", status: "error" }); // B should not start expect(rpc.spawned).toHaveLength(1); const result = await mock.executeTool("TaskGet", { taskId: "2" }); expect(result.content[0].text).toContain("Status: pending"); }); it("tasks without agentType are not cascaded even if unblocked", async () => { await mock.executeTool("TaskCreate", { subject: "Agent task", description: "Desc", agentType: "general-purpose", }); await mock.executeTool("TaskCreate", { subject: "Manual task", description: "Desc", // No agentType — manual }); await mock.executeTool("TaskUpdate", { taskId: "2", addBlockedBy: ["1"] }); await mock.executeTool("TaskExecute", { task_ids: ["1"] }); mock.emitEvent("subagents:completed", { id: "agent-1" }); // Manual task should stay pending expect(rpc.spawned).toHaveLength(1); }); }); describe("Standalone operation (no subagents extension)", () => { let mock: ReturnType; beforeEach(() => { // Init WITHOUT installSubagentsMock — no subagents extension present mock = mockPi(); initExtension(mock.pi as any); }); it("all core task tools are registered", () => { for (const name of ["TaskCreate", "TaskList", "TaskGet", "TaskUpdate", "TaskExecute"]) { expect(mock.tools.has(name)).toBe(true); } }); it("TaskCreate works without subagents", async () => { const result = await mock.executeTool("TaskCreate", { subject: "Write tests", description: "Add unit tests for the parser", }); expect(result.content[0].text).toContain("Write tests"); }); it("TaskList works without subagents", async () => { await mock.executeTool("TaskCreate", { subject: "A", description: "desc" }); await mock.executeTool("TaskCreate", { subject: "B", description: "desc" }); const result = await mock.executeTool("TaskList", {}); expect(result.content[0].text).toContain("#1"); expect(result.content[0].text).toContain("#2"); }); it("TaskGet works without subagents", async () => { await mock.executeTool("TaskCreate", { subject: "Read me", description: "details here" }); const result = await mock.executeTool("TaskGet", { taskId: "1" }); expect(result.content[0].text).toContain("Read me"); expect(result.content[0].text).toContain("details here"); }); it("TaskUpdate works without subagents", async () => { await mock.executeTool("TaskCreate", { subject: "Update me", description: "desc" }); await mock.executeTool("TaskUpdate", { taskId: "1", status: "in_progress" }); const result = await mock.executeTool("TaskGet", { taskId: "1" }); expect(result.content[0].text).toContain("in_progress"); }); it("TaskExecute gracefully refuses without subagents", async () => { await mock.executeTool("TaskCreate", { subject: "Agent task", description: "desc", agentType: "general-purpose", }); const result = await mock.executeTool("TaskExecute", { task_ids: ["1"] }); expect(result.content[0].text).toContain("Subagent execution is currently unavailable"); }); it("subagents lifecycle events are silently ignored without mapped agents", () => { // These should not throw even though no subagents extension is loaded mock.emitEvent("subagents:completed", { id: "ghost-agent", result: "done" }); mock.emitEvent("subagents:failed", { id: "ghost-agent", error: "boom", status: "error" }); // No crash = pass }); it("task dependencies work without subagents", async () => { await mock.executeTool("TaskCreate", { subject: "First", description: "desc" }); await mock.executeTool("TaskCreate", { subject: "Second", description: "desc" }); await mock.executeTool("TaskUpdate", { taskId: "2", addBlockedBy: ["1"] }); const result = await mock.executeTool("TaskGet", { taskId: "2" }); expect(result.content[0].text).toContain("Blocked by"); expect(result.content[0].text).toContain("#1"); }); }); describe("RPC protocol correctness", () => { it("ping uses scoped reply channel (not shared channel)", () => { const mock = mockPi(); const emitted: Array<{ channel: string; data: unknown }> = []; const origEmit = mock.pi.events.emit.bind(mock.pi.events); mock.pi.events.emit = (channel: string, data: unknown) => { emitted.push({ channel, data }); origEmit(channel, data); }; initExtension(mock.pi as any); // Find the ping emit const pingEmit = emitted.find(e => e.channel === "subagents:rpc:ping"); expect(pingEmit).toBeDefined(); const pingData = pingEmit!.data as { requestId: string }; expect(pingData.requestId).toBeDefined(); expect(typeof pingData.requestId).toBe("string"); }); it("spawn reply cleans up listener and timer on success", async () => { const mock = mockPi(); const rpc = installSubagentsMock(mock.pi); initExtension(mock.pi as any); await mock.executeTool("TaskCreate", { subject: "Test", description: "desc", agentType: "general-purpose", }); await mock.executeTool("TaskExecute", { task_ids: ["1"] }); expect(rpc.spawned).toHaveLength(1); // Second spawn should get a fresh requestId (not conflict with first) await mock.executeTool("TaskCreate", { subject: "Test 2", description: "desc", agentType: "general-purpose", }); await mock.executeTool("TaskExecute", { task_ids: ["2"] }); expect(rpc.spawned).toHaveLength(2); expect(rpc.spawned[0].id).not.toBe(rpc.spawned[1].id); rpc.unsub(); }); it("spawn RPC rejects on timeout when no responder exists", async () => { const mock = mockPi(); // Install ping handler (for version check) but no spawn handler installVersionedMock(mock.pi, 2); initExtension(mock.pi as any); await mock.executeTool("TaskCreate", { subject: "Timeout test", description: "desc", agentType: "general-purpose", }); // spawnSubagent has a 30s timeout — we'll advance timers vi.useFakeTimers(); const execPromise = mock.executeTool("TaskExecute", { task_ids: ["1"] }); await vi.advanceTimersByTimeAsync(31000); const result = await execPromise; expect(result.content[0].text).toContain("timeout"); vi.useRealTimers(); }); it("ready broadcast sets subagentsAvailable even after init", async () => { const mock = mockPi(); initExtension(mock.pi as any); // Initially no subagents await mock.executeTool("TaskCreate", { subject: "Test", description: "desc", agentType: "general-purpose", }); let result = await mock.executeTool("TaskExecute", { task_ids: ["1"] }); expect(result.content[0].text).toContain("Subagent execution is currently unavailable"); // Reset task status await mock.executeTool("TaskUpdate", { taskId: "1", status: "pending" }); // Late subagents extension broadcasts ready const rpc = installSubagentsMock(mock.pi); result = await mock.executeTool("TaskExecute", { task_ids: ["1"] }); expect(result.content[0].text).toContain("Launched 1 agent"); rpc.unsub(); }); it("spawn RPC rejects with error message from server", async () => { const mock = mockPi(); installSubagentsMock(mock.pi, { spawnError: "No active session" }); initExtension(mock.pi as any); await mock.executeTool("TaskCreate", { subject: "Err test", description: "desc", agentType: "general-purpose", }); const result = await mock.executeTool("TaskExecute", { task_ids: ["1"] }); expect(result.content[0].text).toContain("No active session"); }); it("stop RPC resolves on success", async () => { const mock = mockPi(); const rpc = installSubagentsMock(mock.pi); initExtension(mock.pi as any); // Spawn a task so we have an agent to stop await mock.executeTool("TaskCreate", { subject: "Stoppable", description: "desc", agentType: "general-purpose", }); await mock.executeTool("TaskExecute", { task_ids: ["1"] }); expect(rpc.spawned).toHaveLength(1); const result = await mock.executeTool("TaskStop", { task_id: "1" }); expect(result.content[0].text).toContain("stopped successfully"); expect(rpc.stopped).toContain("agent-1"); rpc.unsub(); }); it("stop RPC returns false on error (agent not found) without throwing", async () => { const mock = mockPi(); const rpc = installSubagentsMock(mock.pi); initExtension(mock.pi as any); // Create and execute a task, then simulate agent already gone await mock.executeTool("TaskCreate", { subject: "Ghost", description: "desc", agentType: "general-purpose", }); await mock.executeTool("TaskExecute", { task_ids: ["1"] }); // Clear spawned list so the mock's stop handler won't find the agent rpc.spawned.length = 0; // TaskStop should still succeed (stopSubagent catches the error) const result = await mock.executeTool("TaskStop", { task_id: "1" }); expect(result.content[0].text).toContain("stopped successfully"); rpc.unsub(); }); it("stop RPC returns false on timeout without throwing", async () => { const mock = mockPi(); initExtension(mock.pi as any); // Mark subagents as available via ready broadcast, but no stop handler installed mock.pi.events.emit("subagents:ready", {}); await mock.executeTool("TaskCreate", { subject: "Timeout stop", description: "desc", agentType: "general-purpose", }); // Manually set task as in_progress with an agentId (no spawn handler) await mock.executeTool("TaskUpdate", { taskId: "1", status: "in_progress", metadata: { agentType: "general-purpose", agentId: "ghost-agent" }, }); vi.useFakeTimers(); const stopPromise = mock.executeTool("TaskStop", { task_id: "1" }); await vi.advanceTimersByTimeAsync(11000); // Should resolve (not throw) — stopSubagent catches timeout const result = await stopPromise; expect(result.content[0].text).toContain("stopped successfully"); vi.useRealTimers(); }); }); /** Install a ping-only mock with a specific protocol version (or no version for v1). */ function installVersionedMock(pi: { events: MockEventBus }, version?: number) { const unsubPing = pi.events.on("subagents:rpc:ping", (data: unknown) => { const { requestId } = data as { requestId: string }; if (version !== undefined) { pi.events.emit(`subagents:rpc:ping:reply:${requestId}`, { success: true, data: { version } }); } else { // v1 handler — no envelope, no version pi.events.emit(`subagents:rpc:ping:reply:${requestId}`, {}); } }); pi.events.emit("subagents:ready", {}); return { unsub() { unsubPing(); } }; } describe("Protocol version mismatch", () => { it("matching version — no warning", async () => { const mock = mockPi(); installVersionedMock(mock.pi, 2); initExtension(mock.pi as any); // No warning on before_agent_start const ctx = mockCtx(); await mock.fireLifecycle("before_agent_start", {}, ctx); expect(ctx.ui.notify).not.toHaveBeenCalled(); }); it("old handler (no version) — warns about pi-subagents", async () => { const mock = mockPi(); installVersionedMock(mock.pi); // no version = v1 initExtension(mock.pi as any); const ctx = mockCtx(); await mock.fireLifecycle("before_agent_start", {}, ctx); expect(ctx.ui.notify).toHaveBeenCalledWith( expect.stringContaining("pi-subagents is outdated"), "warning", ); }); it("handler ahead (v3) — warns about pi-tasks", async () => { const mock = mockPi(); installVersionedMock(mock.pi, 3); initExtension(mock.pi as any); const ctx = mockCtx(); await mock.fireLifecycle("before_agent_start", {}, ctx); expect(ctx.ui.notify).toHaveBeenCalledWith( expect.stringContaining("pi-tasks is outdated"), "warning", ); }); it("handler behind (v1) — warns about pi-subagents", async () => { const mock = mockPi(); installVersionedMock(mock.pi, 1); initExtension(mock.pi as any); const ctx = mockCtx(); await mock.fireLifecycle("before_agent_start", {}, ctx); expect(ctx.ui.notify).toHaveBeenCalledWith( expect.stringContaining("pi-subagents is outdated"), "warning", ); }); it("warning shown only once", async () => { const mock = mockPi(); installVersionedMock(mock.pi); // v1 — triggers warning initExtension(mock.pi as any); const ctx1 = mockCtx(); await mock.fireLifecycle("before_agent_start", {}, ctx1); expect(ctx1.ui.notify).toHaveBeenCalledOnce(); const ctx2 = mockCtx(); await mock.fireLifecycle("before_agent_start", {}, ctx2); expect(ctx2.ui.notify).not.toHaveBeenCalled(); }); }); describe("Widget agent ID display", () => { let store: TaskStore; let widget: TaskWidget; let ui: ReturnType; function mockUICtx() { const state = { widgets: new Map(), statuses: new Map(), }; const ctx: UICtx = { setWidget(key, content, options) { state.widgets.set(key, { content, options }); }, setStatus(key, text) { state.statuses.set(key, text); }, }; return { ctx, state }; } function mockTheme(): Theme { return { fg: (_color: string, text: string) => text, bold: (text: string) => text, strikethrough: (text: string) => `~~${text}~~`, }; } function renderWidget(state: ReturnType["state"]): string[] { const entry = state.widgets.get("tasks"); if (!entry?.content) return []; const theme = mockTheme(); const tui = { terminal: { columns: 200 } }; return entry.content(tui, theme).render(); } beforeEach(() => { vi.useFakeTimers(); store = new TaskStore(); widget = new TaskWidget(store); ui = mockUICtx(); widget.setUICtx(ui.ctx); }); afterEach(() => { widget.dispose(); vi.useRealTimers(); }); it("shows agent ID for active agent-backed tasks", () => { store.create("Agent task", "Desc", "Running tests", { agentType: "general-purpose", agentId: "abc1234567890" }); store.update("1", { status: "in_progress" }); widget.setActiveTask("1", true); const lines = renderWidget(ui.state); expect(lines[1]).toContain("agent abc12"); expect(lines[1]).toContain("Running tests"); }); it("shows agent ID for non-active in_progress agent-backed tasks", () => { store.create("Agent task", "Desc", undefined, { agentType: "general-purpose", agentId: "xyz9876543210" }); store.update("1", { status: "in_progress" }); // NOT calling setActiveTask — simulates external agent management widget.update(); const lines = renderWidget(ui.state); expect(lines[1]).toContain("agent xyz98"); expect(lines[1]).toContain("Agent task"); }); it("does not show agent ID for tasks without agentId", () => { store.create("Manual task", "Desc"); store.update("1", { status: "in_progress" }); widget.update(); const lines = renderWidget(ui.state); expect(lines[1]).not.toContain("agent"); expect(lines[1]).toContain("Manual task"); }); it("does not show agent ID for pending tasks", () => { store.create("Pending agent task", "Desc", undefined, { agentType: "general-purpose", agentId: "abc12345" }); widget.update(); const lines = renderWidget(ui.state); expect(lines[1]).not.toContain("agent abc"); }); it("does not show agent ID for completed tasks", () => { store.create("Done", "Desc", undefined, { agentType: "general-purpose", agentId: "abc12345" }); store.update("1", { status: "completed" }); widget.update(); const lines = renderWidget(ui.state); expect(lines[1]).not.toContain("agent abc"); }); });