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 { afterEach, describe, expect, it, vi } from "vitest";
import {
	createTestSession,
	sandboxEnv,
	type TestSession,
	withProcessEnv,
} from "../../../test/harness";

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 [];
	}
}

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: TestSession | undefined;

	afterEach(() => {
		vi.doUnmock("node:fs");
		vi.resetModules();
		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-"));
		const env = sandboxEnv(root, {
			env: { PI_FIREFOX_BIDI_DEBUG: "1" },
		});
		vi.resetModules();
		const factory = await withProcessEnv(
			env,
			async () => (await import("../index.ts")).default,
		);
		session = await createTestSession({
			cwd: root,
			extensionFactories: [factory],
			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 trace = records(env);
		const events = trace.map((record) => record.event);
		expect(events).toEqual(
			expect.arrayContaining([
				"session.start",
				"session.shutdown",
				"firefox.command.start",
				"firefox.command.finish",
			]),
		);
		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, {
			env: { PI_FIREFOX_BIDI_DEBUG: "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, {
			env: { PI_FIREFOX_BIDI_DEBUG: "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();
	});
});