/** * boot-extension.ts — the mock `pi` / `ctx` pair the wiring tests use to boot the * REAL extension (src/index.ts) and drive its registered tools directly. * * The 14 pre-existing wiring tests each inline their own near-identical copy of * this. They are deliberately NOT migrated: `test/` is outside the tsconfig * `include`, so that churn would be unchecked by `tsc`, and those copies have * small divergences that would have to be reconciled blind. New files use this. */ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { vi } from "vitest"; export interface BootedPi { pi: any; tools: Map; lifecycle: Map; } /** A mock ExtensionAPI that records every tool and lifecycle handler registered. */ export function makePi(): BootedPi { const tools = new Map(); const lifecycle = new Map(); const pi = { registerMessageRenderer: vi.fn(), registerTool: vi.fn((t: any) => tools.set(t.name, t)), registerCommand: vi.fn(), on: vi.fn((event: string, handler: any) => lifecycle.set(event, handler)), events: { emit: vi.fn(), on: vi.fn(() => vi.fn()), }, appendEntry: vi.fn(), sendMessage: vi.fn(), } as any; return { pi, tools, lifecycle }; } /** A mock ExtensionContext — the second half of what a tool's `execute` receives. */ export function ctx(overrides: Record = {}) { return { // The interactive mode extensions normally run in. Set explicitly because // the `@handle` input hook is TUI-only, so an absent mode would make every // mention test exercise the headless fall-through instead. mode: "tui", hasUI: false, ui: { setStatus: vi.fn(), setWidget: vi.fn(), notify: vi.fn(), addAutocompleteProvider: vi.fn() }, cwd: process.cwd(), model: undefined, modelRegistry: { find: vi.fn(), getAvailable: vi.fn(() => []) }, sessionManager: { getSessionId: vi.fn(() => "s1"), getBranch: vi.fn(() => []) }, getSystemPrompt: vi.fn(() => "parent"), ...overrides, } as any; } /** Text of a tool result. */ export const textOf = (r: any): string => r.content[0].text; /** Let queued microtasks run — enough for the manager's internal chaining. */ export const flush = async () => { await new Promise((r) => setImmediate(r)); await new Promise((r) => setImmediate(r)); }; export interface Hermetic { dir: string; restore: () => void; } /** * Redirect cwd, `PI_CODING_AGENT_DIR` and `HOME` into a fresh temp dir, so the * developer's real settings and agent files can't reach the extension under * test. Call BEFORE instantiating the extension — settings are read at boot. */ export function hermeticDir(opts: { settings?: Record; agentFiles?: Record; } = {}): Hermetic { const dir = mkdtempSync(join(tmpdir(), "pi-boot-")); const agentDir = mkdtempSync(join(tmpdir(), "pi-boot-agentdir-")); const prevCwd = process.cwd(); const prevAgentDir = process.env.PI_CODING_AGENT_DIR; const prevHome = process.env.HOME; mkdirSync(join(dir, ".pi"), { recursive: true }); if (opts.settings) { writeFileSync(join(dir, ".pi", "subagents.json"), JSON.stringify(opts.settings)); } if (opts.agentFiles) { mkdirSync(join(dir, ".pi", "agents"), { recursive: true }); for (const [name, content] of Object.entries(opts.agentFiles)) { writeFileSync(join(dir, ".pi", "agents", `${name}.md`), content); } } process.env.PI_CODING_AGENT_DIR = agentDir; process.env.HOME = agentDir; process.chdir(dir); return { dir, restore() { process.chdir(prevCwd); if (prevAgentDir == null) delete process.env.PI_CODING_AGENT_DIR; else process.env.PI_CODING_AGENT_DIR = prevAgentDir; if (prevHome == null) delete process.env.HOME; else process.env.HOME = prevHome; rmSync(dir, { recursive: true, force: true }); rmSync(agentDir, { recursive: true, force: true }); }, }; }