Files
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

651 lines
27 KiB
TypeScript

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<void>;
automationEvidence(): { cookieHeaderPresent: boolean; cookieReceived: boolean; retained: boolean; outcome?: string } | undefined;
close(): Promise<void>;
}
interface ReadyWaiter {
resolve: () => void;
reject: (error: Error) => void;
timer: ReturnType<typeof setTimeout>;
}
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) => ({
"&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;",
}[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<FixtureServer> {
const fixture = readFileSync(FIXTURE, "utf8");
const ready = new Set<string>();
const waiters = new Map<string, ReadyWaiter>();
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<void>((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<void> {
if (ready.has(name)) return Promise.resolve();
if (waiters.has(name)) throw new Error(`fixture readiness already awaited: ${name}`);
return new Promise<void>((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<void> {
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<void>((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<BrowserSessionManager["open"]>[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<BrowserSessionManager["openSetup"]>[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<void> {
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<BrowserBridge> {
return createBrowserBridge({
cdpUrl: session.config.cdpUrl,
allowedOrigins: [origin],
python,
requestTimeoutMs: 10_000,
}, session.signal);
}
async function runCase(cases: ValidationCase[], name: string, operation: () => Promise<void>): Promise<void> {
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<LiveValidationReport> {
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;
}
}