import { afterEach, beforeEach, describe, expect, it, vi } from "bun:test"; import { AsyncJobManager } from "@oh-my-pi/pi-coding-agent/async/job-manager"; import { Settings } from "@oh-my-pi/pi-coding-agent/config/settings"; import { AgentLifecycleManager } from "@oh-my-pi/pi-coding-agent/registry/agent-lifecycle"; import { AgentRegistry } from "@oh-my-pi/pi-coding-agent/registry/agent-registry"; import { TaskTool } from "@oh-my-pi/pi-coding-agent/task"; import * as discoveryModule from "@oh-my-pi/pi-coding-agent/task/discovery"; import * as executorModule from "@oh-my-pi/pi-coding-agent/task/executor"; import type { AgentDefinition, SingleResult, TaskParams } from "@oh-my-pi/pi-coding-agent/task/types"; import type { ToolSession } from "@oh-my-pi/pi-coding-agent/tools"; const taskAgent: AgentDefinition = { name: "task", description: "General-purpose task agent", systemPrompt: "You are a task agent.", source: "bundled", }; function createSession(options: { manager: AsyncJobManager; settings?: Record; spawns?: string | boolean; }): ToolSession { return { cwd: "/tmp", hasUI: false, settings: Settings.isolated({ "async.enabled": true, ...options.settings }), getSessionFile: () => null, getSessionSpawns: () => options.spawns ?? "*", asyncJobManager: options.manager, } as unknown as ToolSession; } function textOf(result: { content: Array<{ type: string; text?: string }> }): string { const content = result.content.find(part => part.type === "text"); return content?.type === "text" ? (content.text ?? "") : ""; } function resultFor(id: string): SingleResult { return { index: 0, id, agent: "task", agentSource: "bundled", task: "prompt", assignment: "work", exitCode: 0, output: "done", stderr: "", truncated: false, durationMs: 1, tokens: 0, requests: 1, }; } function mockDiscovery(agents: AgentDefinition[] = [taskAgent]): void { vi.spyOn(discoveryModule, "discoverAgents").mockResolvedValue({ agents, projectAgentsDir: null }); } describe("task async preflight", () => { const managers: AsyncJobManager[] = []; beforeEach(() => { AgentRegistry.resetGlobalForTests(); AgentLifecycleManager.resetGlobalForTests(); }); afterEach(async () => { vi.restoreAllMocks(); for (const manager of managers.splice(0)) await manager.dispose({ timeoutMs: 1_000 }); AgentLifecycleManager.resetGlobalForTests(); AgentRegistry.resetGlobalForTests(); }); function manager(): AsyncJobManager { const result = new AsyncJobManager({ onJobComplete: () => {} }); managers.push(result); return result; } it.each([ { name: "Unknown", params: { agent: "missing", name: "Unknown", task: "Work." }, expectation: 'Unknown agent "missing"', }, { name: "Disabled", params: { agent: "task", name: "Disabled", task: "Work." }, settings: { "task.disabledAgents": ["task"] }, expectation: 'Agent "task" is disabled', }, { name: "Disallowed", params: { agent: "task", name: "Disallowed", task: "Work." }, spawns: "scout", expectation: "Cannot spawn 'task'", }, ])( "returns $name policy errors before registering an async job", async ({ name, params, settings, spawns, expectation }) => { mockDiscovery(); const jobs = manager(); const tool = await TaskTool.create(createSession({ manager: jobs, settings, spawns })); const result = await tool.execute("preflight", params as TaskParams); expect(textOf(result)).toContain(expectation); expect(jobs.getJob(name)).toBeUndefined(); }, ); it("reports an invalid batch item synchronously while launching its valid sibling", async () => { mockDiscovery(); const seen: string[] = []; vi.spyOn(executorModule, "runSubprocess").mockImplementation(async options => { seen.push(options.id ?? ""); return resultFor(options.id ?? ""); }); const jobs = manager(); const tool = await TaskTool.create(createSession({ manager: jobs, settings: { "task.batch": true } })); const result = await tool.execute("mixed-preflight", { context: "Shared context.", tasks: [ { name: "Invalid", agent: "missing", task: "Do invalid work." }, { name: "Valid", agent: "task", task: "Do valid work." }, ], } as TaskParams); const text = textOf(result); expect(text).toContain('Task Invalid failed preflight: Unknown agent "missing"'); expect(text).toContain("Spawned agent `Valid`"); expect(text.indexOf("Task Invalid failed preflight")).toBeLessThan(text.indexOf("Spawned agent `Valid`")); expect(jobs.getJob("Invalid")).toBeUndefined(); const valid = jobs.getJob("Valid"); expect(valid).toBeDefined(); await valid!.promise; expect(valid!.status).toBe("completed"); expect(seen).toEqual(["Valid"]); }); });