Luigit
repositories / pi-ext

pi-ext

bugabingas pi extensions

owned by admin

extensions/firefox-bidi/__tests__/debug.test.ts

Raw
import { mkdtempSync, readdirSync, readFileSync, rmSync } from "node:fs";
import { mkdir, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { afterEach, describe, expect, it, vi } from "vitest";
import {
	createPiSession,
	type PiSession,
} from "../../../test/fixture/create-pi-session.ts";

const extensionPath = fileURLToPath(new URL("../index.ts", import.meta.url));

function envValue(env: NodeJS.ProcessEnv, key: string): string {
	const value = env[key];
	if (!value) throw new Error(`missing sandbox environment variable: ${key}`);
	return value;
}

function directory(env: NodeJS.ProcessEnv): string {
	const home = envValue(env, "HOME");
	if (process.platform === "darwin")
		return path.join(home, "Library", "Logs", "pi-ext", "firefox-bidi");
	if (process.platform === "win32")
		return path.join(
			env["LOCALAPPDATA"] ?? path.join(home, "AppData", "Local"),
			"pi-ext",
			"debug",
			"firefox-bidi",
		);
	return path.join(
		envValue(env, "XDG_STATE_HOME"),
		"pi-ext",
		"debug",
		"firefox-bidi",
	);
}

function records(env: NodeJS.ProcessEnv): Record<string, unknown>[] {
	try {
		const files = readdirSync(directory(env)).filter((file) =>
			file.endsWith(".jsonl"),
		);
		return files.flatMap((file) =>
			readFileSync(path.join(directory(env), file), "utf8")
				.trim()
				.split("\n")
				.filter(Boolean)
				.map((line) => JSON.parse(line) as Record<string, unknown>),
		);
	} catch {
		return [];
	}
}

function sandboxEnv(root: string, debug?: "0" | "1"): NodeJS.ProcessEnv {
	const env: NodeJS.ProcessEnv = {
		...process.env,
		HOME: path.join(root, ".test-home"),
		USERPROFILE: path.join(root, ".test-home"),
		XDG_STATE_HOME: path.join(root, ".test-state"),
		APPDATA: path.join(root, ".test-config"),
		LOCALAPPDATA: path.join(root, ".test-state"),
	};
	delete env["PI_CODING_AGENT_DIR"];
	if (debug !== undefined) env["PI_FIREFOX_BIDI_DEBUG"] = debug;
	return env;
}

async function withProcessEnv<T>(
	env: NodeJS.ProcessEnv,
	fn: () => T | Promise<T>,
): Promise<T> {
	const original = { ...process.env };
	for (const key of Object.keys(process.env)) delete process.env[key];
	Object.assign(process.env, env);
	try {
		return await fn();
	} finally {
		for (const key of Object.keys(process.env)) delete process.env[key];
		Object.assign(process.env, original);
	}
}

async function writeSettings(
	env: NodeJS.ProcessEnv,
	debug: unknown,
): Promise<void> {
	const agentDirectory = path.join(envValue(env, "HOME"), ".pi", "agent");
	await mkdir(agentDirectory, { recursive: true });
	await writeFile(
		path.join(agentDirectory, "settings.json"),
		JSON.stringify({ "pi-ext": { debug } }),
	);
}

describe("firefox-bidi debug", () => {
	let root: string | undefined;
	let session: PiSession | undefined;

	afterEach(async () => {
		vi.doUnmock("node:fs");
		vi.resetModules();
		await session?.dispose();
		if (root) rmSync(root, { recursive: true, force: true });
		session = undefined;
		root = undefined;
	});

	it("writes lifecycle events and pairs command spans", async () => {
		root = mkdtempSync(path.join(tmpdir(), "pi-ext-firefox-bidi-debug-"));
		session = await createPiSession({
			cwd: root,
			extensionPaths: [extensionPath],
			env: { PI_FIREFOX_BIDI_DEBUG: "1" },
		});
		const runner = session.session.extensionRunner;
		await runner.emit({ type: "session_start", reason: "startup" });
		await runner
			.getCommand("firefox")
			?.handler("close", runner.createCommandContext());
		await runner.emit({ type: "session_shutdown", reason: "quit" });

		const sandbox = session.env;
		await vi.waitFor(() => {
			const events = records(sandbox).map((record) => record["event"]);
			expect(events).toEqual(
				expect.arrayContaining([
					"session.start",
					"session.shutdown",
					"firefox.command.start",
					"firefox.command.finish",
				]),
			);
		});
		const trace = records(session.env);
		const start = trace.find(
			(record) => record["event"] === "firefox.command.start",
		);
		const finish = trace.find(
			(record) => record["event"] === "firefox.command.finish",
		);
		expect(finish).toMatchObject({
			spanId: start?.["spanId"],
			durationMs: expect.any(Number),
		});
		expect(finish?.["durationMs"]).toBeGreaterThanOrEqual(0);
	});

	it("uses user-level extension selection", async () => {
		root = mkdtempSync(path.join(tmpdir(), "pi-ext-firefox-bidi-debug-"));
		const env = sandboxEnv(root);
		await writeSettings(env, "firefox-bidi");
		vi.resetModules();
		const debug = await withProcessEnv(
			env,
			() => import("../src/pi-ext-debug.ts"),
		);

		await withProcessEnv(env, () => {
			debug.dbg?.("test.selection", { count: 1, payload: "secret" });
			debug.closeDebug();
		});

		const [record] = records(env);
		expect(record).toMatchObject({
			extension: "firefox-bidi",
			event: "test.selection",
			count: 1,
		});
		expect(JSON.stringify(record)).not.toContain("secret");
	});

	it("does no debug work when explicit disable overrides settings", async () => {
		root = mkdtempSync(path.join(tmpdir(), "pi-ext-firefox-bidi-debug-"));
		const env = sandboxEnv(root, "0");
		await writeSettings(env, true);
		vi.resetModules();
		const debug = await withProcessEnv(
			env,
			() => import("../src/pi-ext-debug.ts"),
		);
		let evaluated = false;
		debug.dbg?.(
			"test.disabled",
			(() => {
				evaluated = true;
				return {};
			})(),
		);

		expect(debug.dbg).toBeUndefined();
		expect(evaluated).toBe(false);
		expect(records(env)).toEqual([]);
	});

	it("does not throw when a debug write fails", async () => {
		root = mkdtempSync(path.join(tmpdir(), "pi-ext-firefox-bidi-debug-"));
		const env = sandboxEnv(root, "1");
		const actual = await vi.importActual<typeof import("node:fs")>("node:fs");
		const writeSync = vi.fn(() => {
			throw new Error("write failed");
		});
		vi.doMock("node:fs", () => ({ ...actual, writeSync }));
		vi.resetModules();
		const debug = await withProcessEnv(
			env,
			() => import("../src/pi-ext-debug.ts"),
		);

		await expect(
			withProcessEnv(env, () => {
				debug.dbg?.("test.failure");
				debug.dbg?.("test.failure");
				debug.closeDebug();
			}),
		).resolves.toBeUndefined();
		expect(writeSync).toHaveBeenCalledOnce();
	});
});