import { createServer, type AddressInfo } from "node:net"; import { fetch as realFetch } from "undici"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; let testPort = 0; let prevGatewayPort: string | undefined; const pwMocks = vi.hoisted(() => ({ cookiesGetViaPlaywright: vi.fn(async () => ({ cookies: [{ name: "session", value: "abc123" }], })), storageGetViaPlaywright: vi.fn(async () => ({ values: { token: "value" } })), evaluateViaPlaywright: vi.fn(async () => "ok"), })); const routeCtxMocks = vi.hoisted(() => { const profileCtx = { profile: { cdpUrl: "http://127.0.0.1:9222" }, ensureTabAvailable: vi.fn(async () => ({ targetId: "tab-1", url: "https://example.com", })), stopRunningBrowser: vi.fn(async () => {}), }; return { profileCtx, createBrowserRouteContext: vi.fn(() => ({ state: () => ({ resolved: { evaluateEnabled: false } }), forProfile: vi.fn(() => profileCtx), mapTabError: vi.fn(() => null), })), }; }); vi.mock("../config/config.js", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, loadConfig: () => ({ browser: { enabled: true, evaluateEnabled: false, defaultProfile: "openclaw", profiles: { openclaw: { cdpPort: testPort + 1, color: "#FF4500" }, }, }, }), writeConfigFile: vi.fn(async () => {}), }; }); vi.mock("./pw-ai-module.js", () => ({ getPwAiModule: vi.fn(async () => pwMocks), })); vi.mock("./server-context.js", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, createBrowserRouteContext: routeCtxMocks.createBrowserRouteContext, }; }); async function getFreePort(): Promise { const probe = createServer(); await new Promise((resolve, reject) => { probe.once("error", reject); probe.listen(0, "127.0.0.1", () => resolve()); }); const addr = probe.address() as AddressInfo; await new Promise((resolve) => probe.close(() => resolve())); return addr.port; } describe("browser control evaluate gating", () => { beforeEach(async () => { testPort = await getFreePort(); prevGatewayPort = process.env.OPENCLAW_GATEWAY_PORT; process.env.OPENCLAW_GATEWAY_PORT = String(testPort - 2); pwMocks.cookiesGetViaPlaywright.mockClear(); pwMocks.storageGetViaPlaywright.mockClear(); pwMocks.evaluateViaPlaywright.mockClear(); routeCtxMocks.profileCtx.ensureTabAvailable.mockClear(); routeCtxMocks.profileCtx.stopRunningBrowser.mockClear(); }); afterEach(async () => { vi.restoreAllMocks(); if (prevGatewayPort === undefined) { delete process.env.OPENCLAW_GATEWAY_PORT; } else { process.env.OPENCLAW_GATEWAY_PORT = prevGatewayPort; } const { stopBrowserControlServer } = await import("./server.js"); await stopBrowserControlServer(); }); it("blocks act:evaluate but still allows cookies/storage reads", async () => { const { startBrowserControlServerFromConfig } = await import("./server.js"); await startBrowserControlServerFromConfig(); const base = `http://127.0.0.1:${testPort}`; const evalRes = (await realFetch(`${base}/act`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ kind: "evaluate", fn: "() => 1" }), }).then((r) => r.json())) as { error?: string }; expect(evalRes.error).toContain("browser.evaluateEnabled=false"); expect(pwMocks.evaluateViaPlaywright).not.toHaveBeenCalled(); const cookiesRes = (await realFetch(`${base}/cookies`).then((r) => r.json())) as { ok: boolean; cookies?: Array<{ name: string }>; }; expect(cookiesRes.ok).toBe(true); expect(cookiesRes.cookies?.[0]?.name).toBe("session"); expect(pwMocks.cookiesGetViaPlaywright).toHaveBeenCalledWith({ cdpUrl: "http://127.0.0.1:9222", targetId: "tab-1", }); const storageRes = (await realFetch(`${base}/storage/local?key=token`).then((r) => r.json(), )) as { ok: boolean; values?: Record; }; expect(storageRes.ok).toBe(true); expect(storageRes.values).toEqual({ token: "value" }); expect(pwMocks.storageGetViaPlaywright).toHaveBeenCalledWith({ cdpUrl: "http://127.0.0.1:9222", targetId: "tab-1", kind: "local", key: "token", }); }); });