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; execute: (...args: any[]) => Promise; } interface CapturedPi { tools: RegisteredTool[]; commands: string[]; handlers: string[]; } function sessionManagerFixture(): BrowserSessionManager & { browserExit(id: string): void } { const sessions = new Map(); const setupSessions = new Set(); const controllers = new Map(); 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( values: Record, action: () => Promise, ): Promise { const temporaryAgentDir = values.PI_CODING_AGENT_DIR === undefined ? await mkdtemp(join(tmpdir(), "fast-browser-test-agent-")) : undefined; const effectiveValues: Record = { ...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(); 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 { 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 { 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; 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((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; 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((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; 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((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((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((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((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((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((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(request: BrowserTransportRequest): Promise> { 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; if (request.operation.op === "close") return { op: "close", result: { closed: true } } as unknown as BrowserTransportResultFor; 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()); }); });