import { mkdtemp, readdir, readFile, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, describe, expect, it, vi } from "vitest"; import { createTestSession, sandboxEnv, type TestSession, withProcessEnv, } from "../../../test/harness"; let root = ""; let session: TestSession | 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)); } async function sandbox(debug: "0" | "1"): Promise { root = await mkdtemp(join(tmpdir(), "pi-ext-chrome-cdp-debug-")); return sandboxEnv(root, { env: { PI_CHROME_CDP_DEBUG: debug } }); } afterEach(async () => { vi.doUnmock("node:fs"); 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 () => { const env = await sandbox("1"); vi.resetModules(); const factory = await withProcessEnv( env, async () => (await import("../index.ts")).default, ); session = await createTestSession({ cwd: root, extensionFactories: [factory], 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", }); 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(); }); });