import { AsyncLocalStorage } from "node:async_hooks"; import { execFile } from "node:child_process"; import { rmSync } from "node:fs"; import { appendFile, chmod, copyFile, cp, mkdir, mkdtemp, rm, writeFile, } from "node:fs/promises"; import { tmpdir } from "node:os"; import path, { delimiter } from "node:path"; import { promisify } from "node:util"; import { type Context, fauxAssistantMessage, fauxProvider, InMemoryCredentialStore, } from "@earendil-works/pi-ai"; import type { ExtensionUIContext } from "@earendil-works/pi-coding-agent"; import { stripTerminalSequences } from "@earendil-works/pi-tui"; import { createPiSession, type PiSession, } from "../../../test/fixture/create-pi-session.ts"; import { cleanGitEnvironment } from "../git.js"; import type { Hunk, Plan } from "../types.js"; const repositorySeeds = new Map>(); export async function copySeedRepository( name: string, initialize: (root: string) => Promise, ): Promise { let seed = repositorySeeds.get(name); if (!seed) { const creation = (async () => { const root = await mkdtemp(path.join(tmpdir(), `strata-seed-${name}-`)); try { await initialize(root); process.once("exit", () => rmSync(root, { recursive: true, force: true }), ); return root; } catch (error) { await rm(root, { recursive: true, force: true }); throw error; } })(); seed = creation; repositorySeeds.set(name, creation); void creation.catch(() => { if (repositorySeeds.get(name) === creation) repositorySeeds.delete(name); }); } const source = await seed; const root = await mkdtemp(path.join(tmpdir(), `strata-${name}-`)); try { await rm(root, { recursive: true, force: true }); await cp(source, root, { recursive: true }); cleanupNative(() => rm(root, { recursive: true, force: true })); return root; } catch (error) { await rm(root, { recursive: true, force: true }); throw error; } } const nativeScope = new AsyncLocalStorage<{ signal: AbortSignal; pending: Set>; cancelling: Promise[]; cleanup: AsyncDisposableStack; }>(); // Used by Vitest aroundEach; no runner import, so Playwright can use this file too. export async function nativeTest( run: () => Promise, context: { signal: AbortSignal }, ): Promise { const controller = new AbortController(); const scope = { signal: AbortSignal.any([context.signal, controller.signal]), pending: new Set>(), cancelling: [] as Promise[], cleanup: new AsyncDisposableStack(), }; await nativeScope.run(scope, async () => { try { await run(); } finally { controller.abort(); while (scope.pending.size) await Promise.allSettled([...scope.pending]); await scope.cleanup.disposeAsync(); } }); } export function testSignal(signal?: AbortSignal): AbortSignal { const parent = nativeScope.getStore()?.signal; return parent && signal ? AbortSignal.any([parent, signal]) : (parent ?? signal ?? new AbortController().signal); } export { own as nativeOperation }; function own(work: () => Promise): Promise { const scope = nativeScope.getStore(); let pending: Promise; try { pending = work(); } catch (error) { pending = Promise.reject(error); } scope?.pending.add(pending); void pending.then( () => scope?.pending.delete(pending), () => scope?.pending.delete(pending), ); return pending; } export function cleanupNative(cleanup: () => Promise): void { nativeScope.getStore()?.cleanup.defer(cleanup); } // Register an in-flight cancellation so fake-executable cleanup drains it before // restoring PATH, matching createFixture's abort-driven cancel handler. export function pushCancelling(promise: Promise): void { nativeScope.getStore()?.cancelling.push(promise); } const exec = promisify(execFile); export function git(cwd: string, ...args: string[]): Promise { return own(async () => { const signal = testSignal(); signal.throwIfAborted(); const command = exec("git", args, { cwd, signal, env: cleanGitEnvironment(), }); const closed = new Promise((resolve) => command.child.once("close", () => resolve()), ); try { return (await command).stdout.trim(); } finally { await closed; } }); } export function withFakeGh( program: string, check: () => Promise, ): Promise { return own(() => fakeGh(program, check)); } async function fakeGh( program: string, check: () => Promise, ): Promise { testSignal().throwIfAborted(); const root = await mkdtemp(path.join(tmpdir(), "strata-gh-")); try { const bin = path.join(root, "bin"); await mkdir(bin); const executable = path.join( bin, process.platform === "win32" ? "gh.exe" : "gh", ); await copyFile(process.execPath, executable); await chmod(executable, 0o755); const preload = path.join(root, "fake-gh.cjs"); await writeFile( preload, `if (/^gh(?:\\.exe)?$/i.test(require("node:path").basename(process.execPath)) && process.env.STRATA_FAKE_GH_PRELOAD === __filename) {\nconst original = process.argv.slice(1); original[0] = require("node:path").basename(original[0]); process.argv = [process.execPath, "fake-gh", ...(process.execArgv.includes("-c") ? ["-c"] : []), ...original]; const args = process.argv.slice(2);\n${program}\nprocess.exit(process.exitCode ?? 0);\n}`, ); const previousPath = process.env["PATH"]; const previousOptions = process.env["NODE_OPTIONS"]; const previousPreload = process.env["STRATA_FAKE_GH_PRELOAD"]; process.env["PATH"] = `${bin}${delimiter}${previousPath ?? ""}`; process.env["STRATA_FAKE_GH_PRELOAD"] = preload; process.env["NODE_OPTIONS"] = [ previousOptions, `--require=${JSON.stringify(preload)}`, ] .filter(Boolean) .join(" "); try { await check(); } finally { await Promise.allSettled(nativeScope.getStore()?.cancelling ?? []); if (previousPath === undefined) delete process.env["PATH"]; else process.env["PATH"] = previousPath; if (previousOptions === undefined) delete process.env["NODE_OPTIONS"]; else process.env["NODE_OPTIONS"] = previousOptions; if (previousPreload === undefined) delete process.env["STRATA_FAKE_GH_PRELOAD"]; else process.env["STRATA_FAKE_GH_PRELOAD"] = previousPreload; } } finally { await rm(root, { recursive: true, force: true }); } } export function withFakeExecutable( name: string, program: string, check: () => Promise, ): Promise { return own(() => fakeExecutable(name, program, check)); } async function fakeExecutable( name: string, program: string, check: () => Promise, ): Promise { testSignal().throwIfAborted(); const root = await mkdtemp(path.join(tmpdir(), "strata-executable-")); try { const bin = path.join(root, "bin"); await mkdir(bin); const executable = path.join( bin, process.platform === "win32" ? `${name}.exe` : name, ); await copyFile(process.execPath, executable); await chmod(executable, 0o755); const preload = path.join(root, "fake-executable.cjs"); await writeFile( preload, `const executable = require("node:path").basename(process.execPath).toLowerCase(); if (executable === ${JSON.stringify(name.toLowerCase())} || executable === ${JSON.stringify(`${name.toLowerCase()}.exe`)}) { const original = process.argv.slice(1); original[0] = require("node:path").basename(original[0]); process.argv = [process.execPath, ${JSON.stringify(name)}, ...(process.execArgv.includes("-c") ? ["-c"] : []), ...original]; const args = process.argv.slice(2); ${program} process.exit(process.exitCode ?? 0); }`, ); const previousPath = process.env["PATH"]; const previousOptions = process.env["NODE_OPTIONS"]; process.env["PATH"] = `${bin}${delimiter}${previousPath ?? ""}`; process.env["NODE_OPTIONS"] = [ previousOptions, `--require=${JSON.stringify(preload)}`, ] .filter(Boolean) .join(" "); try { await check(); } finally { await Promise.allSettled(nativeScope.getStore()?.cancelling ?? []); if (previousPath === undefined) delete process.env["PATH"]; else process.env["PATH"] = previousPath; if (previousOptions === undefined) delete process.env["NODE_OPTIONS"]; else process.env["NODE_OPTIONS"] = previousOptions; } } finally { await rm(root, { recursive: true, force: true }); } } export function fixturePlan(hunks: Array>): Plan { return { summary: "Bound retries before using them.", cohorts: [ { title: "Payment retries", layers: hunks.map((hunk, index) => ({ id: `layer-${index + 1}`, title: `Review ${hunk.path}`, summary: `Layer ${index + 1} explains the original captured change.`, hunks: [{ id: hunk.id, summary: `Explanation for ${hunk.path}.` }], flow: ["Contract", "Consumer", "Tests"], })), }, ], }; } type Fixture = { t: { env: NodeJS.ProcessEnv; session: PiSession["session"]; events: { ui: Array<{ method: string; args: unknown[] }>; messages: Array< | PiSession["session"]["messages"][number] | { role: string; customType?: unknown; content?: unknown; display?: unknown; details?: unknown; } >; }; }; root: string; ctx: ReturnType; runner: PiSession["session"]["extensionRunner"]; command: { handler: (args: string, ctx: unknown) => Promise }; faux: ReturnType; notifications: Array<{ text: string; severity: string }>; widgets: Array<{ key: string; content: unknown; options: unknown }>; prompts: string[]; run: (args?: string) => Promise; widgetLines: (width?: number) => string[]; widgetText: (width?: number) => string; calls: number; url: string; close: () => Promise; }; export function openFixture( mode: "tui" | "rpc" | "json" = "tui", ): Promise { return own(() => createFixture(mode)); } async function createFixture(mode: "tui" | "rpc" | "json") { const signal = testSignal(); signal.throwIfAborted(); const root = await copySeedRepository("pi", async (seed) => { await git(seed, "init", "--quiet"); await appendFile( path.join(seed, ".git", "config"), "[user]\n\tname = Strata Test\n\temail = strata@example.invalid\n", ); await writeFile( path.join(seed, "contract.ts"), "export const attempts = 1;\n", ); await writeFile( path.join(seed, "consumer.ts"), "export const retry = false;\n", ); await git(seed, "add", "."); await git(seed, "commit", "--quiet", "-m", "base"); }); let session: PiSession | undefined; let closing: Promise | undefined; const close = () => (closing ??= (async () => { try { await session?.dispose(); } finally { await rm(root, { recursive: true, force: true }); } })()); cleanupNative(close); try { await writeFile( path.join(root, "contract.ts"), "export const attempts = 3;\n", ); await writeFile( path.join(root, "consumer.ts"), "export const retry = true;\n", ); session = await createPiSession({ credentials: new InMemoryCredentialStore(), cwd: root, env: { STRATA_NO_OPEN: "1" }, extensionPaths: [path.resolve(import.meta.dirname, "../index.ts")], }); const active = session; // The owned fixture roots its agent and XDG sandbox under cwd; keep them out // of the repository worktree so clean-worktree checks stay green, since the // sandbox lives outside the repository entirely. await appendFile( path.join(root, ".git", "info", "exclude"), "agent/\n.home/\n.config/\n.cache/\n.test-state/\n.data/\n", ); signal.throwIfAborted(); const runner = active.session.extensionRunner; const emit = runner.emit.bind(runner); runner.emit = ((...args: Parameters) => own(() => emit(...args))) as typeof runner.emit; const faux = fauxProvider({ provider: "strata-integration", models: [{ id: "review", maxTokens: 16_384 }], }); let calls = 0; const prompts: string[] = []; const respond = (context: Context) => { calls++; const message = context.messages.findLast((item) => item.role === "user"); const text = typeof message?.content === "string" ? message.content : (message?.content ?? []) .filter((part) => part.type === "text") .map((part) => part.text) .join(""); prompts.push(text); if (text.startsWith("Organize")) { const hunks = [ ...text.matchAll( /\s* ({ id: id!, path: filePath! })); return fauxAssistantMessage(JSON.stringify(fixturePlan(hunks))); } return fauxAssistantMessage("Three attempts include the first call."); }; faux.setResponses(Array.from({ length: 20 }, () => respond)); const base = runner.createContext(); base.modelRegistry.registerProvider(faux.provider); const model = base.modelRegistry.find("strata-integration", "review"); if (!model) throw new Error("Fixture model missing"); await active.session.setModel(model); const notifications: Array<{ text: string; severity: string }> = []; const widgets: Array<{ key: string; content: unknown; options: unknown }> = []; const uiCalls: Array<{ method: string; args: unknown[] }> = []; const statusMessages: Array<{ role: string; customType?: unknown; content?: unknown; display?: unknown; details?: unknown; }> = []; // pi.sendMessage maps to AgentSession.sendCustomMessage. Intercept it so // print mode writes nothing to stdout while tests still observe the emitted // custom status messages. active.session.sendCustomMessage = (async (message) => { statusMessages.push({ role: "custom", ...message }); }) as typeof active.session.sendCustomMessage; const capturingUi = { notify: (text: string, severity = "info") => { uiCalls.push({ method: "notify", args: [text, severity] }); notifications.push({ text, severity }); }, setWidget: (key: string, content: unknown, options?: unknown) => { uiCalls.push({ method: "setWidget", args: [key, content, options] }); widgets.push({ key, content, options }); }, setStatus: (key: string, text?: string) => { uiCalls.push({ method: "setStatus", args: [key, text] }); }, setFooter: () => {}, confirm: async () => true, }; if (mode === "json") runner.setUIContext(undefined, "json"); else runner.setUIContext(capturingUi as unknown as ExtensionUIContext, mode); const ctx = Object.create(base); Object.defineProperty(ctx, "waitForIdle", { value: async () => {} }); // `extensions` is private on the real runner; reach the registered command // structurally, exactly as the excluded contract test does at runtime. const command = ( runner as unknown as { extensions: Array<{ commands: Map< string, { handler: (args: string, ctx: unknown) => Promise } >; }>; } ).extensions[0]!.commands.get("strata"); if (!command) throw new Error("strata command missing"); const scope = nativeScope.getStore(); const cancel = () => { const stopped = own(() => command.handler("close", ctx)); scope?.cancelling.push(stopped); }; signal.throwIfAborted(); signal.addEventListener("abort", cancel, { once: true }); cleanupNative(async () => { signal.removeEventListener("abort", cancel); }); const run = (args = "") => own(async () => { signal.throwIfAborted(); await command.handler(args, ctx); signal.throwIfAborted(); }); return { t: { env: process.env, session: active.session, events: { ui: uiCalls, get messages() { return [...active.session.messages, ...statusMessages]; }, }, }, root, ctx, runner, command, faux, notifications, widgets, prompts, run, widgetLines(width = 120): string[] { const content = widgets.findLast( (item) => item.key === "strata", )?.content; if (content === undefined) return []; if (Array.isArray(content)) return content as string[]; if (typeof content !== "function") return []; const theme = { bold: (text: string) => text, fg: (_color: string, text: string) => text, }; return content({}, theme).render(width); }, widgetText(width = 120): string { return this.widgetLines(width).map(stripTerminalSequences).join("\n"); }, get calls() { return calls; }, get url(): string { const line = this.widgetLines(160)[1] ?? ""; const match = /\x1b]8;;([^\x1b]+)\x1b\\/.exec(line); if (match?.[1]) return match[1]; const notice = notifications.findLast((item) => item.text.startsWith("Strata review: "), ); if (notice) return notice.text.slice("Strata review: ".length); const message = statusMessages.findLast( (item) => item.role === "custom" && item.customType === "strata-status" && typeof item.content === "string" && item.content.startsWith("Strata review: "), ); if (message && typeof message.content === "string") return message.content.slice("Strata review: ".length); throw new Error(`No review URL: ${line}`); }, close, }; } catch (error) { await close(); throw error; } }