Files
harness-tools/extensions/fast-browser/bridge.test.ts
T
jb fd63b0ccec
fast-browser scenarios / Offline scenario contract tests (push) Failing after 44s
fast-browser scenarios / Local Chromium headless scenarios (push) Failing after 23s
fast-browser-with-jev
2026-09-20 12:24:08 -04:00

190 lines
8.3 KiB
TypeScript

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<string, unknown>[] = [];
readonly stdin = {
write: (line: string): boolean => {
const request = JSON.parse(line) as Record<string, unknown>;
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<string, unknown>): void {
if (this.silent) return;
const operation = request.operation as Record<string, unknown> | 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<string, unknown>)?.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<string, unknown> | 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<string, unknown> | 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);
});