Luigit
repositories / pi-ext

pi-ext

bugabingas pi extensions

owned by admin

extensions/chrome-cdp/__tests__/contract.test.ts

Raw
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");
	});
});