Luigit
repositories / pi-ext

pi-ext

bugabingas pi extensions

owned by admin

extensions/strata/__tests__/fixture.ts

Raw
import { AsyncLocalStorage } from "node:async_hooks";
import { execFile } from "node:child_process";
import {
	appendFile,
	chmod,
	copyFile,
	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,
} from "@earendil-works/pi-ai";
import { stripTerminalSequences } from "@earendil-works/pi-tui";
import { createTestSession } from "../../../test/harness";
import { cleanGitEnvironment } from "../git.js";
import type { Hunk, Plan } from "../types.js";

const nativeScope = new AsyncLocalStorage<{
	signal: AbortSignal;
	pending: Set<Promise<unknown>>;
	cancelling: Promise<unknown>[];
	cleanup: AsyncDisposableStack;
}>();

// Used by Vitest aroundEach; no runner import, so Playwright can use this file too.
export async function nativeTest(
	run: () => Promise<void>,
	context: { signal: AbortSignal },
) {
	const controller = new AbortController();
	const scope = {
		signal: AbortSignal.any([context.signal, controller.signal]),
		pending: new Set<Promise<unknown>>(),
		cancelling: [] as Promise<unknown>[],
		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<T>(work: () => Promise<T>): Promise<T> {
	const scope = nativeScope.getStore();
	let pending: Promise<T>;
	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>): void {
	nativeScope.getStore()?.cleanup.defer(cleanup);
}

const exec = promisify(execFile);
export function git(cwd: string, ...args: string[]) {
	return own(async () => {
		const signal = testSignal();
		signal.throwIfAborted();
		const command = exec("git", args, {
			cwd,
			signal,
			env: cleanGitEnvironment(),
		});
		const closed = new Promise<void>((resolve) =>
			command.child.once("close", () => resolve()),
		);
		try {
			return (await command).stdout.trim();
		} finally {
			await closed;
		}
	});
}

export function withFakeGh(
	program: string,
	check: () => Promise<void>,
): Promise<void> {
	return own(() => fakeGh(program, check));
}

async function fakeGh(
	program: string,
	check: () => Promise<void>,
): Promise<void> {
	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<void>,
): Promise<void> {
	return own(() => fakeExecutable(name, program, check));
}

async function fakeExecutable(
	name: string,
	program: string,
	check: () => Promise<void>,
): Promise<void> {
	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<Pick<Hunk, "id" | "path">>): 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"],
				})),
			},
		],
	};
}

export function openFixture(mode: "tui" | "rpc" | "json" = "tui") {
	return own(() => createFixture(mode));
}

async function createFixture(mode: "tui" | "rpc" | "json") {
	const signal = testSignal();
	signal.throwIfAborted();
	const root = await mkdtemp(path.join(tmpdir(), "strata-pi-"));
	let t: Awaited<ReturnType<typeof createTestSession>> | undefined;
	let closing: Promise<void> | undefined;
	const close = () =>
		(closing ??= (async () => {
			try {
				// Harness dispose does not emit shutdown or await extension work.
				await t?.session.extensionRunner.emit({
					type: "session_shutdown",
					reason: "quit",
				});
			} finally {
				try {
					t?.dispose();
				} finally {
					await rm(root, { recursive: true, force: true });
				}
			}
		})());
	cleanupNative(close);
	try {
		await git(root, "init", "--quiet");
		await appendFile(
			path.join(root, ".git", "config"),
			"[user]\n\tname = Strata Test\n\temail = strata@example.invalid\n",
		);
		await writeFile(
			path.join(root, "contract.ts"),
			"export const attempts = 1;\n",
		);
		await writeFile(
			path.join(root, "consumer.ts"),
			"export const retry = false;\n",
		);
		await git(root, "add", ".");
		await git(root, "commit", "--quiet", "-m", "base");
		await writeFile(
			path.join(root, "contract.ts"),
			"export const attempts = 3;\n",
		);
		await writeFile(
			path.join(root, "consumer.ts"),
			"export const retry = true;\n",
		);
		t = await createTestSession({
			cwd: root,
			env: { STRATA_NO_OPEN: "1" },
			extensions: [path.resolve(import.meta.dirname, "../index.ts")],
		});
		const session = t;
		signal.throwIfAborted();
		const runner = t.session.extensionRunner;
		const emit = runner.emit.bind(runner);
		runner.emit = (...args: Parameters<typeof emit>) =>
			own(() => emit(...args));
		const base = runner.createContext();
		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(
						/<file path="(contract\.ts|consumer\.ts)">\s*<hunk id="([^"]+)"/g,
					),
				].map(([, filePath, 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));
		base.modelRegistry.registerProvider(faux.provider);
		const model = base.modelRegistry.find("strata-integration", "review");
		if (!model) throw new Error("Fixture model missing");
		await t.session.setModel(model);
		const notifications: Array<{ text: string; severity: string }> = [];
		const widgets: Array<{
			key: string;
			content: unknown;
			options: unknown;
		}> = [];
		const ctx = Object.create(base);
		Object.defineProperties(ctx, {
			model: { value: model },
			hasUI: { value: mode !== "json" },
			waitForIdle: { value: async () => {} },
			mode: { value: mode },
			ui: {
				value: {
					...base.ui,
					notify: (text: string, severity = "info") =>
						notifications.push({ text, severity }),
					setWidget: (key: string, content: unknown, options: unknown) => {
						widgets.push({ key, content, options });
					},
				},
			},
		});
		const command = runner.extensions[0].commands.get("strata");
		const scope = nativeScope.getStore();
		const cancel = () => {
			const stopped = own(() => command.handler("cancel", 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,
			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 = session.events.messages.findLast(
					(item) =>
						item.role === "custom" &&
						item.customType === "strata-status" &&
						typeof item.content === "string" &&
						item.content.startsWith("Strata review: "),
				);
				if (message?.role === "custom" && 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;
	}
}