import { mkdtemp, readdir, readFile, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } 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)); let root = ""; let session: PiSession | undefined; async function tracePath(): Promise { const directory = join(root, ".test-state", "pi-ext", "debug", "chrome-cdp"); const entries = await readdir(directory); expect(entries).toHaveLength(1); return join(directory, entries[0]!); } async function records(): Promise[]> { return (await readFile(await tracePath(), "utf8")) .trim() .split("\n") .map((line) => JSON.parse(line)); } function sandboxEnv(dir: string, debug: "0" | "1"): NodeJS.ProcessEnv { return { ...process.env, HOME: join(dir, ".test-home"), USERPROFILE: join(dir, ".test-home"), XDG_STATE_HOME: join(dir, ".test-state"), APPDATA: join(dir, ".test-config"), LOCALAPPDATA: join(dir, ".test-state"), PI_CHROME_CDP_DEBUG: debug, }; } async function withProcessEnv( env: NodeJS.ProcessEnv, fn: () => T | Promise, ): Promise { 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 sandbox(debug: "0" | "1"): Promise { root = await mkdtemp(join(tmpdir(), "pi-ext-chrome-cdp-debug-")); return sandboxEnv(root, debug); } afterEach(async () => { vi.doUnmock("node:fs"); await session?.dispose(); session = undefined; if (root) await rm(root, { recursive: true, force: true }); root = ""; }); describe("chrome-cdp debug", () => { it("writes session lifecycle events to sandbox user state", async () => { root = await mkdtemp(join(tmpdir(), "pi-ext-chrome-cdp-debug-")); session = await createPiSession({ cwd: root, extensionPaths: [extensionPath], env: { PI_CHROME_CDP_DEBUG: "1" }, }); await session.session.extensionRunner.emit({ type: "session_start", reason: "startup", }); await session.session.extensionRunner.emit({ type: "session_shutdown", reason: "quit", }); await vi.waitFor(async () => { expect((await records()).map((record) => record["event"])).toEqual( expect.arrayContaining(["session.start", "session.shutdown"]), ); }); }); it("pairs monotonic operation spans", async () => { const env = await sandbox("1"); vi.resetModules(); await withProcessEnv(env, async () => { const { closeDebug, span } = await import("../src/pi-ext-debug.ts"); const end = span?.("operation", { index: 2, kind: "cdp" }); await new Promise((resolve) => setTimeout(resolve, 1)); end?.("finish", { status: "ok" }); closeDebug(); }); const [start, finish] = await records(); expect(start).toMatchObject({ event: "operation.start", index: 2, kind: "cdp", }); expect(finish).toMatchObject({ event: "operation.finish", spanId: start!["spanId"], status: "ok", }); expect(finish!["durationMs"]).toEqual(expect.any(Number)); }); it("does no debug work when disabled", async () => { const env = await sandbox("0"); vi.resetModules(); await withProcessEnv(env, async () => { const { dbg } = await import("../src/pi-ext-debug.ts"); let built = false; dbg?.( "test.disabled", (() => { built = true; return {}; })(), ); expect(dbg).toBeUndefined(); expect(built).toBe(false); }); await expect(tracePath()).rejects.toMatchObject({ code: "ENOENT" }); }); it("absorbs writer failures", async () => { const env = await sandbox("1"); vi.resetModules(); const actual = await vi.importActual("node:fs"); const writeSync = vi.fn(() => { throw new Error("write failed"); }); vi.doMock("node:fs", () => ({ ...actual, writeSync })); await withProcessEnv(env, async () => { const { dbg } = await import("../src/pi-ext-debug.ts"); expect(() => dbg?.("test.failure")).not.toThrow(); expect(() => dbg?.("test.failure")).not.toThrow(); }); expect(writeSync).toHaveBeenCalledOnce(); }); });