import assert from "node:assert/strict"; import { execFileSync, spawnSync } from "node:child_process"; import { randomBytes } from "node:crypto"; import { existsSync, mkdtempSync, readFileSync, rmSync, statSync } from "node:fs"; import { createServer, type IncomingMessage, type ServerResponse } from "node:http"; import { tmpdir } from "node:os"; import { join, dirname } from "node:path"; import { fileURLToPath } from "node:url"; import { createBrowserBridge } from "./bridge.ts"; import { createBrowserSessionManager, type BrowserSession, type BrowserSessionManager } from "./browser-session.ts"; import { runSteps } from "./controller.ts"; import type { BrowserBridge, Step } from "./contracts.ts"; const ROOT = dirname(fileURLToPath(import.meta.url)); const FIXTURE = join(ROOT, "fixtures", "development-test.html"); const CHROMIUM = "/usr/bin/chromium"; const PINNED_PYTHON = join(process.cwd(), ".pi", "fast-browser-demo", "venv", "bin", "python"); const STARTUP_TIMEOUT_MS = 20_000; const CLOSE_TIMEOUT_MS = 4_000; const READY_TIMEOUT_MS = 8_000; const RUN_TIMEOUT_MS = 15_000; type CaseStatus = "passed" | "failed" | "skipped"; export interface ValidationCase { name: string; status: CaseStatus; detail: string; elapsedMs: number; } export interface ValidationVersions { chromium: string; python: string; browserHarness: string; node: string; } export interface LiveValidationReport { ok: boolean; versions: ValidationVersions; cases: ValidationCase[]; cleanup: { status: CaseStatus; detail: string }; } interface FixtureServer { origin: string; waitForReady(name: string, timeoutMs?: number): Promise; automationEvidence(): { cookieHeaderPresent: boolean; cookieReceived: boolean; retained: boolean; outcome?: string } | undefined; close(): Promise; } interface ReadyWaiter { resolve: () => void; reject: (error: Error) => void; timer: ReturnType; } interface FixtureOptions { liveCookie: string; liveToken: string; isolationA: string; isolationAToken: string; isolationB: string; isolationBToken: string; } function errorText(error: unknown): string { return error instanceof Error ? error.message : String(error); } function boundedError(error: unknown): string { return errorText(error).replace(/\s+/g, " ").slice(0, 300); } function requiredExecutable(path: string): void { if (!existsSync(path)) throw new Error(`required executable is missing: ${path}`); const stats = statSync(path); if (!stats.isFile() || (stats.mode & 0o111) === 0) throw new Error(`required executable is not executable: ${path}`); } function scrubbedEnvironment(): NodeJS.ProcessEnv { const env: NodeJS.ProcessEnv = {}; const names = [ "PATH", "HOME", "USER", "LOGNAME", "LANG", "TMPDIR", "TMP", "TEMP", "XDG_RUNTIME_DIR", "XDG_SESSION_TYPE", "XDG_CURRENT_DESKTOP", "XDG_SESSION_DESKTOP", "SYSTEMROOT", "COMSPEC", "LD_LIBRARY_PATH", "DYLD_LIBRARY_PATH", "DISPLAY", "WAYLAND_DISPLAY", "XAUTHORITY", "DBUS_SESSION_BUS_ADDRESS", "GDK_BACKEND", ]; for (const name of names) { if (process.env[name] !== undefined) env[name] = process.env[name]; } for (const [name, value] of Object.entries(process.env)) { if (name.startsWith("LC_") && value !== undefined) env[name] = value; } env.PYTHONNOUSERSITE = "1"; env.PYTHONUTF8 = "1"; return env; } function commandVersion(command: string, args: string[]): string { try { return execFileSync(command, args, { env: scrubbedEnvironment(), encoding: "utf8", timeout: 5_000, stdio: ["ignore", "pipe", "pipe"] }).trim().split(/\r?\n/, 1)[0] ?? "unknown"; } catch (error) { throw new Error(`could not read runtime version for ${command}: ${boundedError(error)}`); } } function runtimeVersions(python: string): ValidationVersions { requiredExecutable(CHROMIUM); const chromium = commandVersion(CHROMIUM, ["--version"]); const pythonVersion = commandVersion(python, ["--version"]); let browserHarness: string; try { browserHarness = execFileSync(python, ["-c", "import importlib.metadata; print(importlib.metadata.version('browser-harness'))"], { env: scrubbedEnvironment(), encoding: "utf8", timeout: 5_000, stdio: ["ignore", "pipe", "pipe"], }).trim(); } catch (error) { throw new Error(`pinned Python does not provide browser-harness==0.1.13: ${boundedError(error)}`); } if (browserHarness !== "0.1.13") { throw new Error(`browser-harness==0.1.13 is required, found ${browserHarness || "unknown"}`); } return { chromium, python: pythonVersion, browserHarness, node: process.version }; } function pythonExecutable(): string { if (existsSync(PINNED_PYTHON)) { requiredExecutable(PINNED_PYTHON); return PINNED_PYTHON; } throw new Error(`pinned Python is missing: ${PINNED_PYTHON}`); } function cookieValue(header: string | undefined, name: string): string | undefined { if (!header) return undefined; const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const match = header.match(new RegExp(`(?:^|;\\s*)${escaped}=([^;]*)`)); return match?.[1]; } function htmlEscape(value: string): string { return value.replace(/[&<>\"']/g, (character) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'", }[character]!)); } function writeHtml(response: ServerResponse, fixture: string, options: { mode: string; retained: boolean; initialStatus: string; buttonLabel: string; readyCase?: string; setCookie?: string; cookieCheck?: string; cookieForbid?: string; }): void { const content = fixture .replaceAll("__MODE__", htmlEscape(options.mode)) .replaceAll("__RETAINED__", String(options.retained)) .replaceAll("__INITIAL_STATUS__", htmlEscape(options.initialStatus)) .replaceAll("__BUTTON_LABEL__", htmlEscape(options.buttonLabel)) .replaceAll("__COOKIE_CHECK__", htmlEscape(options.cookieCheck ?? "")) .replaceAll("__COOKIE_FORBID__", htmlEscape(options.cookieForbid ?? "")) .replace("__READY_CASE__", JSON.stringify(options.readyCase ?? null)); response.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store", ...(options.setCookie ? { "set-cookie": options.setCookie } : {}), }); response.end(content); } async function createFixtureServer(options: FixtureOptions): Promise { const fixture = readFileSync(FIXTURE, "utf8"); const ready = new Set(); const waiters = new Map(); let automationState: { cookieHeaderPresent: boolean; cookieReceived: boolean; retained: boolean; outcome?: string } | undefined; const noteReady = (name: string): void => { ready.add(name); const waiter = waiters.get(name); if (!waiter) return; clearTimeout(waiter.timer); waiters.delete(name); waiter.resolve(); }; const server = createServer((request: IncomingMessage, response: ServerResponse) => { const parsed = new URL(request.url ?? "/", "http://127.0.0.1"); const cookie = request.headers.cookie; const isLive = cookieValue(cookie, options.liveCookie) === options.liveToken; const isA = cookieValue(cookie, options.isolationA) === options.isolationAToken; const isB = cookieValue(cookie, options.isolationB) === options.isolationBToken; if (parsed.pathname === "/setup") { writeHtml(response, fixture, { mode: "setup", retained: false, initialStatus: "Setup fixture loaded; awaiting synthetic cookie", buttonLabel: "Setup fixture", readyCase: "setup", // This validator explicitly tests named-profile persistence. A session // cookie is intentionally insufficient: Chromium is allowed to drop it // during a graceful browser close, leaving no durable Cookies row. setCookie: `${options.liveCookie}=${options.liveToken}; Max-Age=3600; Path=/; SameSite=Lax`, }); return; } if (parsed.pathname === "/automation") { automationState = { cookieHeaderPresent: Boolean(cookie), cookieReceived: isLive, retained: isLive }; writeHtml(response, fixture, { mode: "retained", retained: isLive, initialStatus: isLive ? "Retained seed cookie present" : "Retained seed cookie missing", buttonLabel: "Verify retained session", readyCase: "automation", }); return; } if (parsed.pathname === "/automation-outcome") { if (automationState) automationState.outcome = (parsed.searchParams.get("status") ?? "").slice(0, 120); response.writeHead(204, { "cache-control": "no-store" }); response.end(); return; } if (parsed.pathname === "/headless") { writeHtml(response, fixture, { mode: "headless", retained: false, initialStatus: "Headless fixture ready", buttonLabel: "Continue", }); return; } if (parsed.pathname === "/isolation-a") { writeHtml(response, fixture, { mode: "isolation-a", retained: isA && !isB, initialStatus: isA && !isB ? "Profile A seed is isolated" : "Profile A seed is missing or crossed", buttonLabel: "Verify Profile A isolation", readyCase: "isolation-a", cookieCheck: options.isolationA, cookieForbid: options.isolationB, setCookie: `${options.isolationA}=${options.isolationAToken}; Path=/; SameSite=Lax`, }); return; } if (parsed.pathname === "/isolation-b") { writeHtml(response, fixture, { mode: "isolation-b", retained: isB && !isA, initialStatus: isB && !isA ? "Profile B seed is isolated" : "Profile B seed is missing or crossed", buttonLabel: "Verify Profile B isolation", readyCase: "isolation-b", cookieCheck: options.isolationB, cookieForbid: options.isolationA, setCookie: `${options.isolationB}=${options.isolationBToken}; Path=/; SameSite=Lax`, }); return; } if (parsed.pathname === "/ready") { const name = parsed.searchParams.get("case"); const valid = name === "automation" ? true : name === "setup" ? isLive : name === "isolation-a" ? isA && !isB : name === "isolation-b" ? isB && !isA : false; if (!name || !valid) { response.writeHead(409, { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" }); response.end("not ready"); return; } noteReady(name); response.writeHead(204, { "cache-control": "no-store" }); response.end(); return; } response.writeHead(404, { "content-type": "text/plain; charset=utf-8" }); response.end("not found"); }); await new Promise((resolve, reject) => { server.once("error", reject); server.listen(0, "127.0.0.1", () => resolve()); }); const address = server.address(); if (!address || typeof address === "string") throw new Error("fixture server did not expose a TCP port"); return { origin: `http://127.0.0.1:${address.port}`, waitForReady(name, timeoutMs = READY_TIMEOUT_MS): Promise { if (ready.has(name)) return Promise.resolve(); if (waiters.has(name)) throw new Error(`fixture readiness already awaited: ${name}`); return new Promise((resolve, reject) => { const timer = setTimeout(() => { waiters.delete(name); reject(new Error(`fixture ready request timed out for ${name}`)); }, timeoutMs); waiters.set(name, { resolve, reject, timer }); }); }, automationEvidence(): { cookieHeaderPresent: boolean; cookieReceived: boolean; retained: boolean; outcome?: string } | undefined { return automationState ? { ...automationState } : undefined; }, async close(): Promise { for (const waiter of waiters.values()) { clearTimeout(waiter.timer); waiter.reject(new Error("fixture server closed before readiness")); } waiters.clear(); server.closeAllConnections?.(); await new Promise((resolve, reject) => { const timer = setTimeout(() => { server.closeAllConnections?.(); resolve(); }, 1_000); server.close((error) => { clearTimeout(timer); if (error && (error as NodeJS.ErrnoException).code !== "ERR_SERVER_NOT_RUNNING") reject(error); else resolve(); }); }); }, }; } function processLinesContaining(value: string): string[] { const result = spawnSync("ps", ["-eo", "pid=,args="], { encoding: "utf8", timeout: 5_000 }); if (result.status !== 0) return []; return String(result.stdout).split(/\r?\n/).filter((line) => line.includes(value)); } function processPid(line: string): number | undefined { const match = line.trim().match(/^(\\d+)\\s/); if (!match) return undefined; const pid = Number(match[1]); return Number.isSafeInteger(pid) && pid > 0 ? pid : undefined; } function profileProcessLine(profile: string): string { const lines = processLinesContaining(`--user-data-dir=${profile}`); if (!lines.length) throw new Error(`no Chromium process was found for owned profile ${profile}`); return lines.join(" | "); } function profileShutdownEvidence(profile: string): string { const lines = processLinesContaining(`--user-data-dir=${profile}`); const descendantPids = lines.map(processPid).filter((pid): pid is number => pid !== undefined); const gpuDescendantPids = lines .filter((line) => line.includes("--type=gpu-process")) .map(processPid) .filter((pid): pid is number => pid !== undefined); const singletonMarker = existsSync(join(profile, "SingletonLock")); // manager.close() returning means its tracked root child exited; these are // only still-running processes carrying this run's private profile path. return `trackedRootExited=true; descendantPids=${JSON.stringify(descendantPids)}; gpuDescendantPids=${JSON.stringify(gpuDescendantPids)}; singletonMarker=${singletonMarker}`; } function assertLaunchFlags(profile: string, expected: "setup" | "visible"): void { const line = profileProcessLine(profile); if (expected === "setup") { assert.doesNotMatch(line, /remote-debugging|--headless/); } else { assert.match(line, /remote-debugging-port=0/); assert.doesNotMatch(line, /--headless/); } } function assertHeadlessOwnedProcess(root: string): void { const line = processLinesContaining(root).find((candidate) => candidate.includes("--user-data-dir=") && candidate.includes("--headless=new")); assert.ok(line, "no owned Chromium process was found for the disposable headless profile"); assert.match(line, /remote-debugging-port=0/); } function launchConfig(origin: string, path: string, profile: string | undefined, headless: boolean): Parameters[0] { return { allowedOrigins: [origin], browser: { mode: "launch", product: "chromium", executablePath: CHROMIUM, headless, startUrl: `${origin}${path}`, ...(profile ? { profile } : {}), }, }; } function setupConfig(origin: string, path: string, profile: string): Parameters[0] { return { allowedOrigins: [origin], browser: { mode: "launch", product: "chromium", executablePath: CHROMIUM, headless: false, startUrl: `${origin}${path}`, profile, }, }; } async function exactRun(bridge: BrowserBridge, steps: Step[], signal: AbortSignal): Promise { let semanticDecisions = 0; const result = await runSteps(steps, { browser: bridge, decide: async () => { semanticDecisions += 1; throw new Error("unexpected semantic browser decision in exact-control validation"); }, }, { signal, maxSteps: steps.length, timeoutMs: RUN_TIMEOUT_MS, minConfidence: 1, }); assert.equal(semanticDecisions, 0, "exact fixture controls must not invoke JEV or a provider"); assert.equal(result.status, "completed", result.reason); assert.equal(result.actionMayHaveExecuted, false); assert.equal(result.steps.every((step) => step.status === "verified" && step.mutationState === "verified"), true); } async function openBridge(session: BrowserSession, python: string, origin: string): Promise { return createBrowserBridge({ cdpUrl: session.config.cdpUrl, allowedOrigins: [origin], python, requestTimeoutMs: 10_000, }, session.signal); } async function runCase(cases: ValidationCase[], name: string, operation: () => Promise): Promise { const started = Date.now(); try { await operation(); cases.push({ name, status: "passed", detail: "verified", elapsedMs: Date.now() - started }); } catch (error) { cases.push({ name, status: "failed", detail: boundedError(error), elapsedMs: Date.now() - started }); } } export async function runLiveValidation(): Promise { const python = pythonExecutable(); const versions = runtimeVersions(python); const root = mkdtempSync(join(tmpdir(), "fast-browser-live-validation-")); const profileRoot = join(root, "profiles"); const leaseDirectory = join(root, "leases"); const disposableRoot = join(root, "disposable"); const runId = randomBytes(5).toString("hex"); const liveProfile = `live-${runId}`; const profileA = `alpha-${runId}`; const profileB = `beta-${runId}`; const options: FixtureOptions = { liveCookie: `fb_live_${runId}`, liveToken: randomBytes(12).toString("hex"), isolationA: `fb_a_${runId}`, isolationAToken: randomBytes(12).toString("hex"), isolationB: `fb_b_${runId}`, isolationBToken: randomBytes(12).toString("hex"), }; let server: FixtureServer | undefined; let manager: BrowserSessionManager | undefined; const sessions: string[] = []; const bridges: BrowserBridge[] = []; const cases: ValidationCase[] = []; let cleanupDetail = "not run"; let cleanupStatus: CaseStatus = "failed"; try { server = await createFixtureServer(options); manager = createBrowserSessionManager({ profileRoot, leaseDirectory, profileDirectory: disposableRoot, startupTimeoutMs: STARTUP_TIMEOUT_MS, closeTimeoutMs: CLOSE_TIMEOUT_MS, }); let setupPersistenceReady = false; await runCase(cases, "visible manual setup seeds persistent cookie and closes fully", async () => { const setup = await manager!.openSetup(setupConfig(server!.origin, "/setup", liveProfile), new AbortController().signal); sessions.push(setup.id); const profile = join(profileRoot, liveProfile); assertLaunchFlags(profile, "setup"); await server!.waitForReady("setup"); await manager!.close(setup.id); // Closed sessions remain in the bounded recent-exit tombstone so callers // receive the intentional terminal reason instead of an indistinguishable // unknown-session error. assert.throws(() => manager!.get(setup.id), /session is no longer available: session closed/); assert.equal(existsSync(profile), true, "named profile must survive setup close"); const ownedProcesses = processLinesContaining(`--user-data-dir=${profile}`); assert.equal(ownedProcesses.length, 0, `manual setup close evidence: ${profileShutdownEvidence(profile)}`); sessions.splice(sessions.indexOf(setup.id), 1); setupPersistenceReady = true; }); if (setupPersistenceReady) { await runCase(cases, "visible automation reopens retained session with bounded fill/click", async () => { const session = await manager!.open(launchConfig(server!.origin, "/automation", liveProfile, false), new AbortController().signal); sessions.push(session.id); const profile = join(profileRoot, liveProfile); assertLaunchFlags(profile, "visible"); const bridge = await openBridge(session, python, server!.origin); bridges.push(bridge); await server!.waitForReady("automation"); try { await exactRun(bridge, [ { instruction: "Display name", action: "fill", text: "Milestone" }, { instruction: "Verify retained session", action: "click", postcondition: { kind: "text", value: "Retained session verified" } }, ], session.signal); } catch (error) { throw new Error(`${boundedError(error)}; expectedPostcondition=Retained session verified; automationEvidence=${JSON.stringify(server!.automationEvidence())}`); } finally { await bridge.close(); bridges.splice(bridges.indexOf(bridge), 1); } await manager!.close(session.id); sessions.splice(sessions.indexOf(session.id), 1); assert.equal(processLinesContaining(`--user-data-dir=${profile}`).length, 0, "automation browser must be fully closed"); }); } else { cases.push({ name: "visible automation reopens retained session with bounded fill/click", status: "skipped", detail: "blocked by visible manual setup cleanup failure; production lifecycle fix required", elapsedMs: 0, }); } await runCase(cases, "headless launch verifies bounded fill/click", async () => { const session = await manager!.open(launchConfig(server!.origin, "/headless", undefined, true), new AbortController().signal); sessions.push(session.id); // The manager intentionally keeps the disposable directory private and // opaque. Its owned temporary root still lets this harness verify flags. assertHeadlessOwnedProcess(root); const bridge = await openBridge(session, python, server!.origin); bridges.push(bridge); try { await exactRun(bridge, [ { instruction: "Display name", action: "fill", text: "Ada" }, { instruction: "Continue", action: "click", postcondition: { kind: "text", value: "Headless bounded action verified" } }, ], session.signal); } finally { await bridge.close(); bridges.splice(bridges.indexOf(bridge), 1); } await manager!.close(session.id); sessions.splice(sessions.indexOf(session.id), 1); }); let alpha: BrowserSession | undefined; let beta: BrowserSession | undefined; let alphaBridge: BrowserBridge | undefined; let betaBridge: BrowserBridge | undefined; await runCase(cases, "two named profiles isolate state, contention rejects, and one survives the other close", async () => { [alpha, beta] = await Promise.all([ manager!.open(launchConfig(server!.origin, "/isolation-a", profileA, true), new AbortController().signal), manager!.open(launchConfig(server!.origin, "/isolation-b", profileB, true), new AbortController().signal), ]); sessions.push(alpha.id, beta.id); await Promise.all([server!.waitForReady("isolation-a"), server!.waitForReady("isolation-b")]); await assert.rejects( manager!.open(launchConfig(server!.origin, "/isolation-a", profileA, true), new AbortController().signal), /active or stale lease/i, ); alphaBridge = await openBridge(alpha, python, server!.origin); betaBridge = await openBridge(beta, python, server!.origin); bridges.push(alphaBridge, betaBridge); await Promise.all([ exactRun(alphaBridge, [{ instruction: "Verify Profile A isolation", action: "click", postcondition: { kind: "text", value: "Profile A isolated" } }], alpha.signal), exactRun(betaBridge, [{ instruction: "Verify Profile B isolation", action: "click", postcondition: { kind: "text", value: "Profile B isolated" } }], beta.signal), ]); await alphaBridge.close(); bridges.splice(bridges.indexOf(alphaBridge), 1); alphaBridge = undefined; await manager!.close(alpha.id); sessions.splice(sessions.indexOf(alpha.id), 1); assert.doesNotThrow(() => manager!.get(beta!.id)); const remaining = await betaBridge!.observe(beta.signal); assert.equal(remaining.url, `${server!.origin}/isolation-b`); assert(remaining.candidates.some((candidate) => candidate.name === "Verify Profile B isolation")); assert(processLinesContaining(`--user-data-dir=${join(profileRoot, profileB)}`).length > 0, "profile B browser must remain alive"); }); if (alphaBridge) await alphaBridge.close().catch(() => {}); if (betaBridge) { await betaBridge.close().catch(() => {}); const index = bridges.indexOf(betaBridge); if (index >= 0) bridges.splice(index, 1); } if (alpha) { await manager.close(alpha.id).catch(() => {}); const index = sessions.indexOf(alpha.id); if (index >= 0) sessions.splice(index, 1); } if (beta) { await manager.close(beta.id).catch(() => {}); const index = sessions.indexOf(beta.id); if (index >= 0) sessions.splice(index, 1); } } finally { for (const bridge of [...bridges]) await bridge.close().catch(() => {}); if (manager) { for (const sessionId of [...sessions]) await manager.close(sessionId).catch(() => {}); await manager.closeAll().catch(() => {}); } const leftovers = processLinesContaining(root); if (server) await server.close().catch(() => {}); // Preserve every failed-case root for postmortem inspection even when all // owned browser processes were safely reaped. Never remove a root that may // still contain an owned process or a failed-case profile/evidence trail. const preserveFailureRoot = cases.some((item) => item.status === "failed") || cases.length === 0; if (leftovers.length === 0 && !preserveFailureRoot) { try { rmSync(root, { recursive: true, force: true }); } catch { /* reported below */ } } const stillExists = existsSync(root); if (leftovers.length === 0) { cleanupStatus = "passed"; cleanupDetail = preserveFailureRoot ? `owned browsers, workers, leases, and profiles cleaned; failed-case root preserved: ${root}` : "owned browsers, workers, leases, profiles, and temporary root cleaned"; } else { cleanupStatus = "failed"; cleanupDetail = `leftovers after owned cleanup: processes=${leftovers.length}, rootExists=${stillExists}`; } } return { ok: cases.every((item) => item.status === "passed") && cleanupStatus === "passed", versions, cases, cleanup: { status: cleanupStatus, detail: cleanupDetail }, }; } function printReport(report: LiveValidationReport): void { console.log("Fast-browser live validation"); console.log(`Chromium: ${report.versions.chromium}`); console.log(`Python: ${report.versions.python}`); console.log(`browser-harness: ${report.versions.browserHarness}`); console.log(`Node: ${report.versions.node}`); for (const item of report.cases) { console.log(`${item.status.toUpperCase()} ${item.name} (${item.elapsedMs}ms): ${item.detail}`); } console.log(`${report.cleanup.status.toUpperCase()} cleanup: ${report.cleanup.detail}`); } if (import.meta.main) { try { const report = await runLiveValidation(); printReport(report); if (!report.ok) process.exitCode = 1; } catch (error) { console.error(`BLOCKED preflight/live validation: ${boundedError(error)}`); process.exitCode = 1; } }