Files
harness-tools/extensions/fast-browser/index.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

881 lines
38 KiB
TypeScript

import assert from "node:assert/strict";
import { mkdtemp, rm } from "node:fs/promises";
import { mkdtempSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import test, { mock } from "node:test";
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
import registerFastBrowser, { type FastBrowserAdapters } from "./index.ts";
import type { BrowserSession, BrowserSetupSession, BrowserSessionManager } from "./browser-session.ts";
import type {
BrowserBridge,
BrowserOperation,
BrowserTransport,
BrowserTransportRequest,
BrowserTransportResultFor,
BrowserRuntime,
Candidate,
Observation,
} from "./contracts.ts";
import { createBrowserRuntime } from "./runtime.ts";
// Adapter tests are intentionally offline. In particular, exact-name runs must
// not accidentally call JEV or any other HTTP provider using ambient secrets.
const forbiddenFetch = mock.method(globalThis, "fetch", async () => {
throw new Error("Network is disabled in offline adapter tests");
});
test.after(() => assert.equal(forbiddenFetch.mock.callCount(), 0, "offline tests attempted HTTP"));
interface RegisteredTool {
name: string;
description: string;
executionMode?: string;
promptGuidelines?: string[];
parameters: Record<string, any>;
execute: (...args: any[]) => Promise<any>;
}
interface CapturedPi {
tools: RegisteredTool[];
commands: string[];
handlers: string[];
}
function sessionManagerFixture(): BrowserSessionManager & { browserExit(id: string): void } {
const sessions = new Map<string, BrowserSession | BrowserSetupSession>();
const setupSessions = new Set<string>();
const controllers = new Map<string, AbortController>();
let next = 0;
return {
async open(config, signal) {
const controller = new AbortController();
const session: BrowserSession = {
id: `session-${++next}`,
config: {
...config,
cdpUrl: config.cdpUrl ?? `http://127.0.0.1:${9_222 + next}`,
},
owned: config.browser?.mode === "launch",
signal: AbortSignal.any([signal, controller.signal]),
};
sessions.set(session.id, session);
controllers.set(session.id, controller);
return session;
},
async openSetup(config, signal) {
const controller = new AbortController();
const session: BrowserSetupSession = {
id: `setup-${++next}`,
config: { ...config, browser: config.browser ? { ...config.browser } : undefined },
owned: true,
signal: AbortSignal.any([signal, controller.signal]),
};
sessions.set(session.id, session);
setupSessions.add(session.id);
controllers.set(session.id, controller);
return session;
},
get(id) {
const session = sessions.get(id);
if (!session) throw new Error("unknown session");
if (setupSessions.has(id)) throw new Error("manual-only and cannot be automated");
return session as BrowserSession;
},
async close(id) {
controllers.get(id)?.abort(new Error("session closed"));
controllers.delete(id);
setupSessions.delete(id);
sessions.delete(id);
},
async closeAll() {
for (const controller of controllers.values()) controller.abort(new Error("sessions closed"));
controllers.clear();
setupSessions.clear();
sessions.clear();
},
browserExit(id: string) {
controllers.get(id)?.abort(new Error("browser exited"));
controllers.delete(id);
setupSessions.delete(id);
sessions.delete(id);
},
};
}
function capturePi(): CapturedPi {
const captured: CapturedPi = { tools: [], commands: [], handlers: [] };
registerFastBrowser(
{
registerTool(tool: unknown) {
captured.tools.push(tool as RegisteredTool);
},
registerCommand(name: string) {
captured.commands.push(name);
},
on(name: string) {
captured.handlers.push(name);
},
} as unknown as ExtensionAPI,
{ createSessionManager: (() => sessionManagerFixture()) as FastBrowserAdapters["createSessionManager"] },
);
return captured;
}
async function withEnvironment<T>(
values: Record<string, string | undefined>,
action: () => Promise<T>,
): Promise<T> {
const temporaryAgentDir = values.PI_CODING_AGENT_DIR === undefined
? await mkdtemp(join(tmpdir(), "fast-browser-test-agent-"))
: undefined;
const effectiveValues: Record<string, string | undefined> = {
...values,
...(temporaryAgentDir ? { PI_CODING_AGENT_DIR: temporaryAgentDir } : {}),
};
const names = [
"PI_CODING_AGENT_DIR",
"FAST_BROWSER_CDP_URL",
"FAST_BROWSER_ALLOWED_ORIGINS",
"FAST_BROWSER_PYTHON",
"FAST_BROWSER_TEXT_MODEL",
"FAST_BROWSER_TEXT_REASONING",
"TYPESAFE_API_KEY",
"FAST_BROWSER_TEST_PASSWORD",
];
const previous = new Map<string, string | undefined>();
for (const name of names) {
previous.set(name, process.env[name]);
const value = effectiveValues[name];
if (value === undefined) delete process.env[name];
else process.env[name] = value;
}
try {
return await action();
} finally {
for (const [name, value] of previous) {
if (value === undefined) delete process.env[name];
else process.env[name] = value;
}
if (temporaryAgentDir) await rm(temporaryAgentDir, { recursive: true, force: true });
}
}
function configEnvironment(): Record<string, string> {
return {
FAST_BROWSER_CDP_URL: "http://127.0.0.1:9222",
FAST_BROWSER_ALLOWED_ORIGINS: '["https://example.test"]',
};
}
function fixtureCandidate(name = "Password"): Candidate {
return { id: "candidate-1", role: "textbox", name, actions: ["fill"] };
}
function fixtureObservation(candidate = fixtureCandidate()): Observation {
return {
observationId: "observation-1",
url: "https://example.test/login?token=private#fragment",
title: "Fixture",
candidates: [candidate],
};
}
function fakeContext(overrides: Partial<ExtensionContext> = {}): ExtensionContext {
return {
// This is print mode on purpose. The adapter must not use hasUI as an
// approval gate and must not call the UI confirm method.
hasUI: false,
mode: "print",
cwd: mkdtempSync(join(tmpdir(), "fast-browser-test-project-")),
ui: {
confirm: async () => {
throw new Error("UI confirmation must never be called");
},
},
modelRegistry: {
getAvailable: () => [],
hasConfiguredAuth: () => false,
},
...overrides,
} as unknown as ExtensionContext;
}
function bridgeFixture(
onAct: (request: unknown) => void = () => undefined,
): BrowserBridge {
return {
async observe() {
return fixtureObservation();
},
async act(request) {
onAct(request);
return { verified: true, evidence: "field value verified" };
},
async close() {
// no-op
},
};
}
function registerWithAdapters(adapters: FastBrowserAdapters): CapturedPi {
const captured: CapturedPi = { tools: [], commands: [], handlers: [] };
registerFastBrowser(
{
registerTool(tool: unknown) {
captured.tools.push(tool as RegisteredTool);
},
registerCommand(name: string) {
captured.commands.push(name);
},
on(name: string) {
captured.handlers.push(name);
},
} as unknown as ExtensionAPI,
{ createSessionManager: (() => sessionManagerFixture()) as FastBrowserAdapters["createSessionManager"], ...adapters },
);
return captured;
}
function toolsOf(captured: CapturedPi): { observe: RegisteredTool; run: RegisteredTool } {
const observe = captured.tools.find((tool) => tool.name === "fast_browser_observe");
const run = captured.tools.find((tool) => tool.name === "fast_browser_run");
assert.ok(observe);
assert.ok(run);
return { observe, run };
}
test("registers unattended parallel session tools and the companion command", () => {
const captured = capturePi();
assert.deepEqual(captured.tools.map((tool) => tool.name), [
"fast_browser_open",
"fast_browser_close",
"fast_browser_observe",
"fast_browser_run",
"fast_browser_read",
"fast_browser_assert",
"fast_browser_control",
"fast_browser_capture_evidence",
]);
assert.deepEqual(captured.commands, ["fast-browser"]);
assert.deepEqual(captured.handlers, ["session_shutdown"]);
const { observe, run } = toolsOf(captured);
const open = captured.tools.find((tool) => tool.name === "fast_browser_open");
const close = captured.tools.find((tool) => tool.name === "fast_browser_close");
assert.ok(open);
assert.ok(close);
assert.ok(open.parameters.properties.settings);
const originSchema = open.parameters.properties.settings.properties.allowedOrigins;
assert.ok(originSchema.anyOf?.some((schema: any) => schema.const === "*"));
assert.equal(open.parameters.properties.settings.properties.browser.properties.profile.pattern, "^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$");
assert.equal(close.parameters.properties.sessionId.minLength, 1);
assert.equal(observe.promptGuidelines, undefined);
assert.equal(run.promptGuidelines, undefined);
assert.equal(observe.executionMode, "parallel");
assert.equal(run.executionMode, "parallel");
assert.match(run.description, /caller-authorized/);
assert.match(run.description, /No user dialog/);
assert.match(run.description, /print and JSON/);
const stepsSchema = run.parameters.properties.steps;
assert.equal(stepsSchema.maxItems, 10);
assert.equal(stepsSchema.minItems, 1);
assert.equal(stepsSchema.items.properties.credential.maxLength, 100);
assert.ok(run.parameters.properties.settings);
assert.ok(run.parameters.properties.task);
assert.ok(run.parameters.properties.credentials);
assert.ok(run.parameters.properties.budgets);
const control = captured.tools.find((tool) => tool.name === "fast_browser_control");
assert.ok(control);
assert.equal(control.parameters.allOf.length, 2);
assert.ok(control.parameters.allOf[0].properties.sessionId);
assert.ok(control.parameters.allOf[0].properties.settings);
assert.equal(control.parameters.allOf[1].properties.operation.anyOf.length, 6);
const evidence = captured.tools.find((tool) => tool.name === "fast_browser_capture_evidence");
assert.ok(evidence);
const evidenceOperationSchema = evidence.parameters.allOf[1].properties.operation;
const evidenceOperations = evidenceOperationSchema.enum ?? evidenceOperationSchema.anyOf?.map((schema: any) => schema.const) ?? [];
assert.ok(evidenceOperations.includes("closeTab"));
assert.equal(evidenceOperations.includes("close"), false);
});
test("defaults missing origin policy to unrestricted websites", async () => {
let bridgeOptions: unknown;
const captured = registerWithAdapters({
createBrowserBridge: ((options: unknown) => {
bridgeOptions = options;
return bridgeFixture();
}) as FastBrowserAdapters["createBrowserBridge"],
});
const { observe } = toolsOf(captured);
await withEnvironment({
FAST_BROWSER_CDP_URL: "http://127.0.0.1:9222",
FAST_BROWSER_ALLOWED_ORIGINS: undefined,
}, async () => {
await observe.execute("observe", { settings: {} }, undefined, undefined, fakeContext());
});
assert.deepEqual(bridgeOptions, {
cdpUrl: "http://127.0.0.1:9222",
allowedOrigins: "*",
});
});
test("wildcard sessions narrow per call while restricted sessions cannot broaden", async () => {
let manager: BrowserSessionManager;
let bridgeOptions: unknown;
const captured = registerWithAdapters({
createSessionManager: (() => {
manager = sessionManagerFixture();
return manager;
}) as FastBrowserAdapters["createSessionManager"],
createBrowserBridge: ((options: unknown) => {
bridgeOptions = options;
return bridgeFixture();
}) as FastBrowserAdapters["createBrowserBridge"],
});
const open = captured.tools.find((tool) => tool.name === "fast_browser_open")!;
const close = captured.tools.find((tool) => tool.name === "fast_browser_close")!;
const { observe } = toolsOf(captured);
await withEnvironment({
FAST_BROWSER_CDP_URL: "http://127.0.0.1:9222",
FAST_BROWSER_ALLOWED_ORIGINS: undefined,
}, async () => {
const wildcard = await open.execute("open", { settings: {} }, undefined, undefined, fakeContext());
await observe.execute("observe", {
sessionId: wildcard.details.sessionId,
settings: { allowedOrigins: ["https://narrow.example"] },
}, undefined, undefined, fakeContext());
assert.deepEqual((bridgeOptions as any).allowedOrigins, ["https://narrow.example"]);
// Narrowing is per-call; omitting settings later still uses the stored
// wildcard policy and must not accidentally persist the narrower list.
await observe.execute("observe", {
sessionId: wildcard.details.sessionId,
settings: {},
}, undefined, undefined, fakeContext());
assert.equal((bridgeOptions as any).allowedOrigins, "*");
await close.execute("close", { sessionId: wildcard.details.sessionId }, undefined, undefined, fakeContext());
const restricted = await open.execute("open", {
settings: { allowedOrigins: ["https://restricted.example"] },
}, undefined, undefined, fakeContext());
await assert.rejects(
observe.execute("observe", {
sessionId: restricted.details.sessionId,
settings: { allowedOrigins: "*" },
}, undefined, undefined, fakeContext()),
/failed/,
);
await close.execute("close", { sessionId: restricted.details.sessionId }, undefined, undefined, fakeContext());
});
});
test("does not connect while the extension or command is registered", () => {
let created = 0;
const captured = registerWithAdapters({
createBrowserBridge: (() => {
created++;
return bridgeFixture();
}) as FastBrowserAdapters["createBrowserBridge"],
});
assert.deepEqual(captured.commands, ["fast-browser"]);
assert.equal(created, 0);
});
test("opens an isolated launch session, reuses it by opaque id, and closes it", async () => {
let manager: BrowserSessionManager;
const captured = registerWithAdapters({
createSessionManager: (() => {
manager = sessionManagerFixture();
return manager;
}) as FastBrowserAdapters["createSessionManager"],
createBrowserBridge: (() => bridgeFixture()) as FastBrowserAdapters["createBrowserBridge"],
});
const open = captured.tools.find((tool) => tool.name === "fast_browser_open")!;
const close = captured.tools.find((tool) => tool.name === "fast_browser_close")!;
const { observe } = toolsOf(captured);
await withEnvironment(configEnvironment(), async () => {
const opened = await open.execute("open", {
settings: {
allowedOrigins: ["https://example.test", "https://other.test"],
browser: { mode: "launch", product: "chromium", headless: true, profile: "evebot", startUrl: "https://example.test/login" },
},
}, undefined, undefined, fakeContext());
const sessionId = opened.details.sessionId;
assert.match(sessionId, /^session-/);
assert.equal(opened.details.browser.mode, "launch");
assert.equal(opened.details.browser.profile, "evebot");
const observed = await observe.execute("observe", { sessionId, settings: { allowedOrigins: ["https://other.test"] } }, undefined, undefined, fakeContext());
assert.equal(observed.details.url, "https://example.test");
const closed = await close.execute("close", { sessionId }, undefined, undefined, fakeContext());
assert.deepEqual(closed.details, { sessionId, closed: true });
assert.throws(() => manager!.get(sessionId), /unknown session/);
});
});
test("closes an attached session lease without closing the external browser", async () => {
let manager: BrowserSessionManager;
const captured = registerWithAdapters({
createSessionManager: (() => {
manager = sessionManagerFixture();
return manager;
}) as FastBrowserAdapters["createSessionManager"],
});
const open = captured.tools.find((tool) => tool.name === "fast_browser_open")!;
const close = captured.tools.find((tool) => tool.name === "fast_browser_close")!;
await withEnvironment(configEnvironment(), async () => {
const opened = await open.execute("open", { settings: { allowedOrigins: ["https://example.test"] } }, undefined, undefined, fakeContext());
assert.equal(opened.details.owned, false);
const sessionId = opened.details.sessionId;
const closed = await close.execute("close", { sessionId }, undefined, undefined, fakeContext());
assert.equal(closed.details.closed, true);
assert.throws(() => manager!.get(sessionId), /unknown session/);
});
});
test("manual setup sessions remain explicitly closable", async () => {
let manager: ReturnType<typeof sessionManagerFixture>;
const captured = registerWithAdapters({
createSessionManager: (() => {
manager = sessionManagerFixture();
return manager;
}) as FastBrowserAdapters["createSessionManager"],
});
const close = captured.tools.find((tool) => tool.name === "fast_browser_close")!;
const setup = await manager!.openSetup({} as any, new AbortController().signal);
const result = await close.execute("close", { sessionId: setup.id }, undefined, undefined, fakeContext());
assert.equal(result.details.closed, true);
});
test("rejected session lane cleanup does not create an unhandled rejection", async () => {
const captured = registerWithAdapters({
createBrowserRuntime: (() => ({
async read() { throw new Error("expected read failure"); },
async close() {},
}) as unknown as BrowserRuntime) as FastBrowserAdapters["createBrowserRuntime"],
});
const open = captured.tools.find((tool) => tool.name === "fast_browser_open")!;
const read = captured.tools.find((tool) => tool.name === "fast_browser_read")!;
const close = captured.tools.find((tool) => tool.name === "fast_browser_close")!;
await withEnvironment(configEnvironment(), async () => {
const opened = await open.execute("open", { settings: { allowedOrigins: ["https://example.test"] } }, undefined, undefined, fakeContext());
const unhandled: unknown[] = [];
const listener = (reason: unknown) => unhandled.push(reason);
process.on("unhandledRejection", listener);
try {
await assert.rejects(read.execute("read", { sessionId: opened.details.sessionId }, undefined, undefined, fakeContext()), /fast_browser_read failed/);
await new Promise<void>((resolve) => setImmediate(resolve));
assert.deepEqual(unhandled, []);
} finally {
process.off("unhandledRejection", listener);
await close.execute("close", { sessionId: opened.details.sessionId }, undefined, undefined, fakeContext());
}
});
});
test("browser-exit cleanup failures remain actionable", async () => {
let manager: ReturnType<typeof sessionManagerFixture>;
const captured = registerWithAdapters({
createSessionManager: (() => {
manager = sessionManagerFixture();
return manager;
}) as FastBrowserAdapters["createSessionManager"],
createBrowserRuntime: (() => ({
sessionId: "unused",
async close() { throw new Error("runtime cleanup failed"); },
}) as unknown as BrowserRuntime) as FastBrowserAdapters["createBrowserRuntime"],
});
const open = captured.tools.find((tool) => tool.name === "fast_browser_open")!;
const close = captured.tools.find((tool) => tool.name === "fast_browser_close")!;
await withEnvironment(configEnvironment(), async () => {
const opened = await open.execute("open", { settings: { allowedOrigins: ["https://example.test"] } }, undefined, undefined, fakeContext());
manager!.browserExit(opened.details.sessionId);
await new Promise<void>((resolve) => setImmediate(resolve));
await assert.rejects(close.execute("close", { sessionId: opened.details.sessionId }, undefined, undefined, fakeContext()), /runtime cleanup failed/);
});
});
test("natural browser exit removes only that session runtime and store", async () => {
let manager: ReturnType<typeof sessionManagerFixture>;
const closed: string[] = [];
const captured = registerWithAdapters({
createSessionManager: (() => {
manager = sessionManagerFixture();
return manager;
}) as FastBrowserAdapters["createSessionManager"],
createBrowserRuntime: ((options) => ({
sessionId: options.sessionId,
async close() { closed.push(options.sessionId); },
}) as unknown as BrowserRuntime) as FastBrowserAdapters["createBrowserRuntime"],
});
const open = captured.tools.find((tool) => tool.name === "fast_browser_open")!;
const close = captured.tools.find((tool) => tool.name === "fast_browser_close")!;
await withEnvironment(configEnvironment(), async () => {
const first = await open.execute("open", { settings: { allowedOrigins: ["https://example.test"] } }, undefined, undefined, fakeContext());
const second = await open.execute("open", { settings: { allowedOrigins: ["https://example.test"] } }, undefined, undefined, fakeContext());
manager!.browserExit(first.details.sessionId);
await assert.doesNotReject(close.execute("close", { sessionId: first.details.sessionId }, undefined, undefined, fakeContext()));
await new Promise<void>((resolve) => setImmediate(resolve));
assert.deepEqual(closed, [first.details.sessionId]);
assert.throws(() => manager!.get(first.details.sessionId), /unknown session/);
await close.execute("close", { sessionId: second.details.sessionId }, undefined, undefined, fakeContext());
assert.deepEqual(closed, [first.details.sessionId, second.details.sessionId]);
});
});
test("closing one active session aborts only that session's run", async () => {
let manager: BrowserSessionManager;
let started!: () => void;
const runStarted = new Promise<void>((resolve) => { started = resolve; });
const captured = registerWithAdapters({
createSessionManager: (() => {
manager = sessionManagerFixture();
return manager;
}) as FastBrowserAdapters["createSessionManager"],
createBrowserBridge: (() => bridgeFixture()) as FastBrowserAdapters["createBrowserBridge"],
runSteps: (async (_steps, _deps, options) => {
started();
await new Promise<void>((resolve) => {
if (options?.signal?.aborted) return resolve();
options?.signal?.addEventListener("abort", () => resolve(), { once: true });
});
return { status: "stopped", reason: "session closed", steps: [], elapsedMs: 0 };
}) as FastBrowserAdapters["runSteps"],
});
const open = captured.tools.find((tool) => tool.name === "fast_browser_open")!;
const close = captured.tools.find((tool) => tool.name === "fast_browser_close")!;
const { run } = toolsOf(captured);
await withEnvironment(configEnvironment(), async () => {
const settings = { allowedOrigins: ["https://example.test"], browser: { mode: "launch" as const, product: "chromium" as const, startUrl: "https://example.test/login" } };
const first = await open.execute("open", { settings }, undefined, undefined, fakeContext());
const second = await open.execute("open", { settings }, undefined, undefined, fakeContext());
const runPromise = run.execute("run", { sessionId: first.details.sessionId, steps: [{ instruction: "Password", action: "fill", text: "known" }] }, undefined, undefined, fakeContext());
await runStarted;
await close.execute("close", { sessionId: first.details.sessionId }, undefined, undefined, fakeContext());
const result = await runPromise;
assert.equal(result.details.reason, "session closed");
assert.doesNotThrow(() => manager!.get(second.details.sessionId));
await close.execute("close", { sessionId: second.details.sessionId }, undefined, undefined, fakeContext());
});
});
test("guards overlap per session while allowing independent sessions to run", async () => {
let manager: BrowserSessionManager;
let startedCount = 0;
let releaseStarted!: () => void;
const startedPromise = new Promise<void>((resolve) => { releaseStarted = resolve; });
const captured = registerWithAdapters({
createSessionManager: (() => {
manager = sessionManagerFixture();
return manager;
}) as FastBrowserAdapters["createSessionManager"],
createBrowserBridge: (() => bridgeFixture()) as FastBrowserAdapters["createBrowserBridge"],
runSteps: (async (_steps, _deps, options) => {
startedCount++;
if (startedCount === 2) releaseStarted();
await new Promise<void>((resolve) => {
if (options?.signal?.aborted) return resolve();
options?.signal?.addEventListener("abort", () => resolve(), { once: true });
});
return { status: "stopped", reason: "released", steps: [], elapsedMs: 0 };
}) as FastBrowserAdapters["runSteps"],
});
const open = captured.tools.find((tool) => tool.name === "fast_browser_open")!;
const close = captured.tools.find((tool) => tool.name === "fast_browser_close")!;
const { run } = toolsOf(captured);
await withEnvironment(configEnvironment(), async () => {
const settings = { allowedOrigins: ["https://example.test"], browser: { mode: "launch" as const, product: "chromium" as const, startUrl: "https://example.test/login" } };
const first = await open.execute("open", { settings }, undefined, undefined, fakeContext());
const second = await open.execute("open", { settings }, undefined, undefined, fakeContext());
const input = { steps: [{ instruction: "Password", action: "fill", text: "known" }] };
const firstRun = run.execute("run", { ...input, sessionId: first.details.sessionId }, undefined, undefined, fakeContext());
await new Promise<void>((resolve) => {
const check = () => startedCount > 0 ? resolve() : setImmediate(check);
check();
});
const queuedFirstRun = run.execute("run", { ...input, sessionId: first.details.sessionId }, undefined, undefined, fakeContext());
const secondRun = run.execute("run", { ...input, sessionId: second.details.sessionId }, undefined, undefined, fakeContext());
await startedPromise;
await close.execute("close", { sessionId: first.details.sessionId }, undefined, undefined, fakeContext());
await close.execute("close", { sessionId: second.details.sessionId }, undefined, undefined, fakeContext());
await Promise.all([firstRun, queuedFirstRun, secondRun]);
assert.equal(startedCount, 3);
});
});
test("rejects launch mode on the no-session legacy path", async () => {
const captured = registerWithAdapters({
createBrowserBridge: (() => {
throw new Error("bridge must not be created");
}) as FastBrowserAdapters["createBrowserBridge"],
});
const { observe } = toolsOf(captured);
await withEnvironment(configEnvironment(), async () => {
await assert.rejects(
observe.execute("observe", {
settings: {
allowedOrigins: ["https://example.test"],
browser: { mode: "launch", product: "chromium", startUrl: "https://example.test/login" },
},
}, undefined, undefined, fakeContext()),
/fast_browser_observe failed.*fast_browser_open/,
);
});
});
test("observe validates caller settings before spawning and forwards allowedOrigins", async () => {
let created = 0;
let bridgeOptions: unknown;
const captured = registerWithAdapters({
createBrowserBridge: ((options: unknown) => {
created++;
bridgeOptions = options;
return bridgeFixture();
}) as FastBrowserAdapters["createBrowserBridge"],
});
const { observe } = toolsOf(captured);
await withEnvironment(configEnvironment(), async () => {
const result = await observe.execute(
"observe",
{ settings: { allowedOrigins: ["https://caller-authorized.test"] } },
AbortSignal.timeout(1000),
undefined,
fakeContext(),
);
assert.equal(result.details.url, "https://example.test");
assert.deepEqual(bridgeOptions, {
cdpUrl: "http://127.0.0.1:9222",
allowedOrigins: ["https://caller-authorized.test"],
});
});
await withEnvironment(configEnvironment(), async () => {
await assert.rejects(
observe.execute(
"observe",
{ settings: { allowedOrigins: ["https://caller-authorized.test/path"] } },
undefined,
undefined,
fakeContext(),
),
/fast_browser_observe failed/,
);
});
assert.equal(created, 1, "invalid settings spawned a bridge");
});
test("runs a caller-authorized credential fill in print mode without UI approval", async () => {
let actionRequest: any;
const captured = registerWithAdapters({
createBrowserBridge: ((() => bridgeFixture((request) => {
actionRequest = request;
})) as FastBrowserAdapters["createBrowserBridge"]),
});
const { run } = toolsOf(captured);
await withEnvironment(configEnvironment(), async () => {
const result = await run.execute(
"run",
{
task: "Fill the password on the caller-authorized test fixture",
steps: [{ instruction: "Password", action: "fill", credential: "password" }],
credentials: { password: "literal-secret" },
},
undefined,
undefined,
fakeContext(),
);
assert.equal(result.details.status, "completed");
assert.equal(actionRequest.text, "literal-secret");
// The resolved value is allowed only at the browser fill boundary. It is
// absent from the compact result and tool content.
assert.equal(JSON.stringify(result.details).includes("literal-secret"), false);
assert.equal(result.content[0].text.includes("literal-secret"), false);
});
});
test("resolves an environment credential reference only when its fill is selected", async () => {
let actionRequest: any;
const captured = registerWithAdapters({
createBrowserBridge: (() => bridgeFixture((request) => {
actionRequest = request;
})) as FastBrowserAdapters["createBrowserBridge"],
});
const { run } = toolsOf(captured);
await withEnvironment(
{ ...configEnvironment(), FAST_BROWSER_TEST_PASSWORD: "env-secret" },
async () => {
const result = await run.execute(
"run",
{
steps: [{ instruction: "Password", action: "fill", credential: "password" }],
credentials: { password: { env: "FAST_BROWSER_TEST_PASSWORD" } },
},
undefined,
undefined,
fakeContext(),
);
assert.equal(result.details.status, "completed");
assert.equal(actionRequest.text, "env-secret");
assert.equal(result.content[0].text.includes("env-secret"), false);
},
);
});
test("passes run budgets to the controller and keeps the exact-name path offline", async () => {
let received: { steps: unknown; deps: any; options: any } | undefined;
const captured = registerWithAdapters({
createBrowserBridge: (() => bridgeFixture()) as FastBrowserAdapters["createBrowserBridge"],
runSteps: (async (steps, deps, options) => {
received = { steps, deps, options };
return { status: "completed", reason: "fixture", steps: [], elapsedMs: 0 };
}) as FastBrowserAdapters["runSteps"],
});
const { run } = toolsOf(captured);
await withEnvironment(configEnvironment(), async () => {
await run.execute(
"run",
{
steps: [{ instruction: "Password", action: "fill", text: "known-value" }],
budgets: { maxSteps: 3, timeoutMs: 3_000, minConfidence: 0.9 },
},
undefined,
undefined,
fakeContext(),
);
});
assert.ok(received);
assert.equal(received.options.maxSteps, 3);
assert.equal(received.options.timeoutMs, 3_000);
assert.equal(received.options.minConfidence, 0.9);
assert.equal(typeof received.deps.resolveCredential, "function");
});
test("an unavailable auxiliary model does not break literal or credential fills", async () => {
let actionRequest: any;
const captured = registerWithAdapters({
createBrowserBridge: (() => bridgeFixture((request) => { actionRequest = request; })) as FastBrowserAdapters["createBrowserBridge"],
});
const { run } = toolsOf(captured);
await withEnvironment({ ...configEnvironment(), FAST_BROWSER_TEXT_MODEL: "missing/model" }, async () => {
const result = await run.execute("run", {
steps: [{ instruction: "Password", action: "fill", credential: "password" }],
credentials: { password: "literal-secret" },
}, undefined, undefined, fakeContext());
assert.equal(result.details.status, "completed");
assert.equal(actionRequest.text, "literal-secret");
});
});
test("does not expose credential values through semantic decisions", async () => {
let decisionInput: any;
const captured = registerWithAdapters({
createBrowserBridge: (() => ({
async observe() {
return fixtureObservation({
...fixtureCandidate("Password field"),
id: "password-field",
});
},
async act() {
return { verified: true, evidence: "field value verified" };
},
async close() {},
})) as FastBrowserAdapters["createBrowserBridge"],
createJevDecider: (() => async (input: unknown) => {
decisionInput = input;
return { candidateId: "password-field", confidence: 1, model: "offline-fixture" };
}) as unknown as FastBrowserAdapters["createJevDecider"],
});
const { run } = toolsOf(captured);
await withEnvironment({ ...configEnvironment(), TYPESAFE_API_KEY: "typesafe-fixture" }, async () => {
const result = await run.execute(
"run",
{
steps: [{ instruction: "enter the password", action: "fill", credential: "password" }],
credentials: { password: "password-secret" },
},
undefined,
undefined,
fakeContext(),
);
assert.equal(result.details.status, "completed");
});
assert.ok(decisionInput);
assert.equal(JSON.stringify(decisionInput).includes("password-secret"), false);
});
test("assertion and control results expose only origin-safe tab projections", async () => {
const secretUrl = "https://example.test/private?token=secret-token#fragment";
const tab = { tabId: "tab-1" as never, kind: "page", selected: true, owned: true, url: secretUrl, origin: "https://example.test", title: "Fixture" };
const captured = registerWithAdapters({
createBrowserRuntime: (() => ({
async assert() { return { status: "satisfied", readOnly: true, tab, documentId: "doc-1", assertion: { value: secretUrl } }; },
async navigate() { return { status: "committed", mutationState: "verified", tab, documentId: "doc-2", assertion: { value: secretUrl } }; },
async reload() { return { status: "committed", mutationState: "verified", tab, documentId: "doc-3", assertion: { value: secretUrl } }; },
async close() {},
}) as unknown as BrowserRuntime) as FastBrowserAdapters["createBrowserRuntime"],
});
const open = captured.tools.find((tool) => tool.name === "fast_browser_open")!;
const close = captured.tools.find((tool) => tool.name === "fast_browser_close")!;
const assertTool = captured.tools.find((tool) => tool.name === "fast_browser_assert")!;
const control = captured.tools.find((tool) => tool.name === "fast_browser_control")!;
await withEnvironment(configEnvironment(), async () => {
const opened = await open.execute("open", { settings: { allowedOrigins: ["https://example.test"] } }, undefined, undefined, fakeContext());
const sessionId = opened.details.sessionId;
const assertion = await assertTool.execute("assert", { sessionId, assertion: { kind: "url", match: "exact", value: secretUrl }, wait: "none" }, undefined, undefined, fakeContext());
const navigate = await control.execute("control", { sessionId, operation: { op: "navigate", request: { url: "https://example.test/next?token=secret-token" } } }, undefined, undefined, fakeContext());
const reload = await control.execute("control", { sessionId, operation: { op: "reload", request: {} } }, undefined, undefined, fakeContext());
for (const result of [assertion.details, navigate.details, reload.details]) {
const json = JSON.stringify(result);
assert.equal(json.includes("secret-token"), false);
assert.equal(json.includes("/private"), false);
assert.equal(json.includes("#fragment"), false);
assert.equal(result.tab.url, undefined);
assert.equal(result.tab.origin, "https://example.test");
}
await close.execute("close", { sessionId }, undefined, undefined, fakeContext());
});
});
test("runtime-backed tools reuse one typed transport and preserve explicit tab targets", async () => {
const calls: BrowserTransportRequest[] = [];
const transport: BrowserTransport = {
async dispatch<Operation extends BrowserOperation>(request: BrowserTransportRequest<Operation>): Promise<BrowserTransportResultFor<Operation>> {
calls.push(request as BrowserTransportRequest);
if (request.operation.op === "read") return {
op: "read",
result: {
tab: { tabId: "tab-1" as never, kind: "page", selected: true, owned: true, url: "https://example.test/form", origin: "https://example.test", title: "Fixture" },
documentId: "doc-1", url: "https://example.test/form", title: "Fixture", visibleText: "Visible", visibleTextTruncated: false,
},
} as unknown as BrowserTransportResultFor<Operation>;
if (request.operation.op === "close") return { op: "close", result: { closed: true } } as unknown as BrowserTransportResultFor<Operation>;
throw new Error("unexpected operation");
},
async close() {},
};
const captured = registerWithAdapters({
createBrowserRuntime: ((options, parentSignal) => createBrowserRuntime(options, parentSignal, { create() { return transport; } })) as FastBrowserAdapters["createBrowserRuntime"],
});
const open = captured.tools.find((tool) => tool.name === "fast_browser_open")!;
const read = captured.tools.find((tool) => tool.name === "fast_browser_read")!;
const close = captured.tools.find((tool) => tool.name === "fast_browser_close")!;
await withEnvironment(configEnvironment(), async () => {
const opened = await open.execute("open", { settings: { allowedOrigins: ["https://example.test"] } }, undefined, undefined, fakeContext());
const input = { sessionId: opened.details.sessionId, tabId: "tab-1" };
const first = await read.execute("read", input, undefined, undefined, fakeContext());
const second = await read.execute("read", input, undefined, undefined, fakeContext());
assert.equal(first.details.visibleText, "Visible");
assert.equal(second.details.documentId, "doc-1");
assert.equal(calls.filter((call) => call.operation.op === "read").length, 2);
assert.equal((calls[0]?.operation as any).op, "read");
await close.execute("close", { sessionId: opened.details.sessionId }, undefined, undefined, fakeContext());
});
});