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[] { 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), ); } catch { return []; } } async function writeSettings( env: NodeJS.ProcessEnv, debug: unknown, ): Promise { 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("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(); }); });