import assert from "node:assert/strict"; import { EventEmitter } from "node:events"; import { test } from "node:test"; import { createBrowserBridge, createBrowserTransport, type ChildProcessLike, type SpawnLike } from "./bridge.ts"; import { BrowserOperationError } from "./contracts.ts"; class FakeWorker extends EventEmitter implements ChildProcessLike { readonly requests: Record[] = []; readonly stdin = { write: (line: string): boolean => { const request = JSON.parse(line) as Record; this.requests.push(request); queueMicrotask(() => this.respond(request)); return true; }, end: (): void => undefined, once: (event: string, listener: (...args: any[]) => void): unknown => this.once(event, listener), }; readonly stdout = new EventEmitter() as EventEmitter & { setEncoding?: (encoding: string) => void }; readonly stderr = new EventEmitter(); killed = false; silent = false; kill(): boolean { this.killed = true; this.emit("close", null, "SIGTERM"); return true; } respond(request: Record): void { if (this.silent) return; const operation = request.operation as Record | undefined; const op = operation?.op ?? request.op; const result = op === "observe" ? { observationId: "obs-1", url: "http://fixture.test/form", title: "Fixture", candidates: [{ id: "c-0", role: "textbox", name: "Email", actions: ["fill"] }], } : op === "act" ? { verified: true, evidence: "field value verified" } : { closed: true }; this.stdout.emit("data", JSON.stringify({ id: request.id, ok: true, result: { op, result } }) + "\n"); } } function makeSpawn(worker: FakeWorker, seen: { command?: string; args?: readonly string[]; env?: NodeJS.ProcessEnv }): SpawnLike { return (command, args, options) => { seen.command = command; seen.args = args; seen.env = options.env; return worker; }; } const base = { cdpUrl: "http://127.0.0.1:9222", allowedOrigins: ["http://fixture.test"] }; test("uses one persistent worker and validates observe/fill output", async () => { const worker = new FakeWorker(); const seen: { command?: string; args?: readonly string[]; env?: NodeJS.ProcessEnv } = {}; const bridge = createBrowserBridge({ ...base, python: "/fixture/python", spawn: makeSpawn(worker, seen) }); const observation = await bridge.observe(new AbortController().signal); assert.equal(observation.url, "http://fixture.test/form"); assert.equal(observation.candidates[0]?.name, "Email"); const result = await bridge.act({ observationId: observation.observationId, candidateId: "c-0", action: "fill", text: "literal@example.test", }, new AbortController().signal); assert.equal(result.verified, true); assert.equal(seen.command, "/fixture/python"); assert.match(String(seen.args?.[0]), /browser_worker\.py$/); assert.equal(seen.env?.BH_TELEMETRY, "0"); assert.equal(seen.env?.BROWSER_USE_API_KEY, undefined); assert.equal(worker.requests.length, 2); await bridge.close(); assert.equal((worker.requests.at(-1)?.operation as Record)?.op, "close"); }); test("unrestricted policy accepts any HTTP(S) observation and URL postcondition", async () => { const worker = new FakeWorker(); worker.respond = (request) => { const result = ((request.operation as Record | undefined)?.op ?? request.op) === "observe" ? { observationId: "obs-1", url: "https://other.example/path", title: "Other", candidates: [] } : { verified: true, evidence: "URL postcondition verified" }; worker.stdout.emit("data", JSON.stringify({ id: request.id, ok: true, result: { op: ((request.operation as Record | undefined)?.op ?? request.op), result } }) + "\n"); }; const bridge = createBrowserBridge({ ...base, allowedOrigins: "*", spawn: makeSpawn(worker, {}) }); const observation = await bridge.observe(new AbortController().signal); assert.equal(observation.url, "https://other.example/path"); await bridge.act({ observationId: observation.observationId, candidateId: "c-0", action: "click", postcondition: { kind: "url", value: "https://different.example/next" }, }, new AbortController().signal); await bridge.close(); }); test("does not forward model/provider credentials to the worker", async () => { const names = ["TYPESAFE_API_KEY", "OPENAI_API_KEY", "ANTHROPIC_API_KEY"]; const previous = new Map(names.map((name) => [name, process.env[name]])); for (const name of names) process.env[name] = "should-not-cross-process-boundary"; try { const worker = new FakeWorker(); const seen: { command?: string; args?: readonly string[]; env?: NodeJS.ProcessEnv } = {}; const bridge = createBrowserBridge({ ...base, spawn: makeSpawn(worker, seen) }); assert.equal(seen.env?.TYPESAFE_API_KEY, undefined); assert.equal(seen.env?.OPENAI_API_KEY, undefined); assert.equal(seen.env?.ANTHROPIC_API_KEY, undefined); assert.equal(seen.env?.BH_TELEMETRY, "0"); await bridge.close(); } finally { for (const name of names) { const value = previous.get(name); if (value === undefined) delete process.env[name]; else process.env[name] = value; } } }); test("rejects non-loopback endpoints and click actions without postconditions before spawning", async () => { const worker = new FakeWorker(); assert.throws(() => createBrowserBridge({ ...base, cdpUrl: "https://remote.test:9222", spawn: makeSpawn(worker, {}) }), /loopback/); const bridge = createBrowserBridge({ ...base, spawn: makeSpawn(worker, {}) }); assert.throws(() => bridge.act({ observationId: "obs-1", candidateId: "c-0", action: "click", }, new AbortController().signal), /postcondition/); assert.equal(worker.requests.length, 0); await bridge.close(); }); test("aborting an in-flight request tears down without retrying", async () => { const worker = new FakeWorker(); worker.silent = true; const bridge = createBrowserBridge({ ...base, spawn: makeSpawn(worker, {}), requestTimeoutMs: 500 }); const controller = new AbortController(); const request = bridge.observe(controller.signal); await Promise.resolve(); controller.abort(); await assert.rejects(request, (error: Error) => error.name === "AbortError"); assert.equal(worker.requests.length, 1); assert.equal(worker.killed, true); }); test("malformed worker failure envelope rejects without escaping the event handler", async () => { const worker = new FakeWorker(); worker.respond = (request) => worker.stdout.emit("data", JSON.stringify({ id: request.id, ok: false, error: 42 }) + "\n"); const bridge = createBrowserBridge({ ...base, spawn: makeSpawn(worker, {}) }); await assert.rejects(bridge.observe(new AbortController().signal), /invalid result envelope/); assert.equal(worker.killed, true); await bridge.close(); }); test("duplicate candidate handles and extra fields terminate the worker", async () => { const worker = new FakeWorker(); worker.respond = (request) => worker.stdout.emit("data", JSON.stringify({ id: request.id, ok: true, result: { op: "observe", result: { observationId: "obs-1", url: "http://fixture.test/form", title: "Fixture", candidates: Array(2).fill({ id: "same", role: "textbox", name: "Email", actions: ["fill"] }), } } }) + "\n"); const bridge = createBrowserBridge({ ...base, spawn: makeSpawn(worker, {}) }); await assert.rejects(bridge.observe(new AbortController().signal), /duplicate/); await bridge.close(); }); test("close waits for actual child exit before releasing the browser lane", async () => { const worker = new FakeWorker(); let exited = false; worker.kill = () => { setTimeout(() => { exited = true; worker.emit("close", null, "SIGTERM"); }, 10); return true; }; const bridge = createBrowserBridge({ ...base, spawn: makeSpawn(worker, {}) }); await bridge.observe(new AbortController().signal); await bridge.close(); assert.equal(exited, true); }); test("worker timeout tears down instead of replaying an action", async () => { const worker = new FakeWorker(); worker.silent = true; const bridge = createBrowserBridge({ ...base, spawn: makeSpawn(worker, {}), requestTimeoutMs: 50 }); await assert.rejects(bridge.observe(new AbortController().signal), /timed out/); assert.equal(worker.requests.length, 1); assert.equal(worker.killed, true); });