import { fileURLToPath } from "node:url"; import { initTheme, keyHint } from "@earendil-works/pi-coding-agent"; import { afterEach, beforeAll, 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)); describe("chrome-cdp Pi integration", () => { let session: PiSession | undefined; beforeAll(() => initTheme(undefined, false)); afterEach(async () => { await session?.dispose(); session = undefined; vi.restoreAllMocks(); }); it("registers an inactive tool and toggles it without starting Chrome", async () => { session = await createPiSession({ extensionPaths: [extensionPath] }); const runner = session.session.extensionRunner; const extension = session.extensions[0]; expect(extension?.path).toBe(extensionPath); const tool = extension?.tools.get("chrome_cdp")?.definition; expect(tool?.description).toContain("Chrome DevTools Protocol"); expect(tool?.annotations?.readOnlyHint).toBe(false); expect(tool?.exposure).toBe("deferred"); expect( tool?.promptGuidelines?.some((line: string) => line.includes("sessionId"), ), ).toBe(true); expect(session.session.getActiveToolNames()).not.toContain("chrome_cdp"); const command = runner.getCommand("chrome"); expect(command).toBeDefined(); const ctx = { ...runner.createCommandContext(), mode: "print" as const, hasUI: false, }; await command?.handler("", ctx); expect(session.session.getActiveToolNames()).toContain("chrome_cdp"); await command?.handler("", ctx); expect(session.session.getActiveToolNames()).not.toContain("chrome_cdp"); await command?.handler("close", ctx); expect(session.session.getActiveToolNames()).not.toContain("chrome_cdp"); expect(tool?.exposure).toBe("deferred"); }); it("warns cheaply when Chrome binary is unavailable", async () => { session = await createPiSession({ extensionPaths: [extensionPath], userSettings: { "chrome-cdp": { executablePath: "/nonexistent/chrome-bin" }, }, }); const runner = session.session.extensionRunner; const command = runner.getCommand("chrome"); const ctx = { ...runner.createCommandContext(), mode: "print" as const, hasUI: false, }; const notify = vi.spyOn(ctx.ui, "notify"); await command?.handler("", ctx); expect(session.session.getActiveToolNames()).toContain("chrome_cdp"); const unavailable = notify.mock.calls.find((call) => String(call[0]).includes("Chrome unavailable: Chrome not found"), ); expect(unavailable?.[1]).toBe("warning"); }); it("warns when FFmpeg is missing without disabling browser control", async () => { session = await createPiSession({ extensionPaths: [extensionPath], env: { PATH: "" }, }); const runner = session.session.extensionRunner; const notify = vi.spyOn(runner.createCommandContext().ui, "notify"); await runner.emit({ type: "session_start", reason: "startup" }); expect(notify).toHaveBeenCalledWith( expect.stringContaining( "FFmpeg not found on PATH; video recording unavailable", ), "warning", ); expect(session.extensions[0]?.tools.has("chrome_cdp")).toBe(true); expect(session.errors).toEqual([]); }); it("renders commands without exposing CDP parameters", async () => { session = await createPiSession({ extensionPaths: [extensionPath] }); const tool = session.extensions[0]?.tools.get("chrome_cdp")?.definition; const theme = { fg: (_color: string, value: string) => value, bold: (value: string) => value, }; const call = tool?.renderCall?.( { operations: [ { kind: "cdp", method: "Browser.getVersion", params: { marker: "private-value" }, }, ], }, theme as never, { expanded: true } as never, ); const text = call?.render(100).join("\n"); expect(text).toContain("Browser.getVersion"); expect(text).not.toContain("private-value"); }); it("labels batches, advertises expansion, and previews results only when expanded", async () => { session = await createPiSession({ extensionPaths: [extensionPath] }); const tool = session.extensions[0]?.tools.get("chrome_cdp")?.definition; expect(tool).toBeDefined(); const theme = { fg: (_color: string, value: string) => value, bold: (value: string) => value, }; const args = { operations: [ { kind: "cdp", method: "Page.navigate", params: { url: "https://private.example" }, }, { kind: "wait", event: "Page.loadEventFired" }, ], }; const call = (expanded: boolean) => tool ?.renderCall?.(args as never, theme as never, { expanded } as never) .render(100) .join("\n"); expect(call(false)).toContain(" Chrome"); expect(call(false)).toContain("Page.navigate"); expect(call(false)).toContain("wait Page.loadEventFired"); expect(call(false)).toContain(keyHint("app.tools.expand", "to expand")); expect(call(false)).not.toContain("private.example"); expect(call(true)).not.toContain("to expand"); const result = { content: [ { type: "text" as const, text: JSON.stringify({ output: [ { command: "Browser.getVersion", raw: { id: 2, result: { product: "Chrome/154" } }, }, ], events: [], }), }, ], details: { completed: 1, total: 1, eventCount: 0, operations: ["Browser.getVersion"], }, }; const rendered = (expanded: boolean) => tool ?.renderResult?.( result, { expanded, isPartial: false } as never, theme as never, { isError: false } as never, ) .render(100) .join("\n"); expect(rendered(false)).not.toContain("Chrome/154"); expect(rendered(true)).toContain("Chrome/154"); expect(rendered(true)).toContain("Browser.getVersion"); const status = { ...result, details: { ...result.details, operations: ["status"], browser: "Chrome/154", pages: 2, }, }; const statusText = tool ?.renderResult?.( status, { expanded: false, isPartial: false } as never, theme as never, { isError: false } as never, ) .render(100) .join("\n"); expect(statusText).toContain("Chrome/154 ยท 2 pages"); }); });