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

423 lines
26 KiB
TypeScript

import assert from "node:assert/strict";
import { access, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { createServer } from "node:net";
import { join } from "node:path";
import test from "node:test";
import { parseScenarioCliArgs, runScenarioCli, ScenarioCliError, waitForScenarioHttpReadiness } from "./scenario-cli.ts";
import { BrowserOperationError } from "./contracts.ts";
import type { BrowserRuntime, BrowserTabId, PrivateArtifactStore } from "./contracts.ts";
import type { BrowserSession, BrowserSessionManager } from "./browser-session.ts";
const scenario = JSON.stringify({
version: 1,
id: "local-form",
settings: {
allowedOrigins: ["http://fixture.test"],
browser: {
mode: "launch",
product: "chromium",
executablePath: "/definitely/missing/fixture-browser",
startUrl: "http://fixture.test/form.html"
}
},
operations: [
{ op: "step", step: { instruction: "Display name", action: "fill", text: "Ada" } },
{ op: "step", step: { instruction: "Continue", action: "click", postcondition: { kind: "text", value: "Fixture complete" } } },
],
});
async function temporary(fn: (directory: string) => Promise<void>): Promise<void> {
const directory = await mkdtemp(join(tmpdir(), "fast-browser-cli-test-"));
try { await fn(directory); } finally { await rm(directory, { recursive: true, force: true }); }
}
test("parses explicit scenario paths, bounded matrix options, and headless defaults", () => {
const options = parseScenarioCliArgs(["--scenario", "one.json", "two.json", "--parallel=2", "--json", "--capture-on-request", "--check"]);
assert.deepEqual(options.scenarioPaths, ["one.json", "two.json"]);
assert.equal(options.parallel, 2);
assert.equal(options.headless, true);
assert.equal(options.format, "json");
assert.equal(options.checkOnly, true);
assert.equal(options.captureOnRequest, true);
assert.throws(() => parseScenarioCliArgs(["--headless", "--visible", "one.json"]), ScenarioCliError);
assert.throws(() => parseScenarioCliArgs(["--parallel", "0", "one.json"]), ScenarioCliError);
assert.throws(() => parseScenarioCliArgs(["--unknown", "one.json"]), ScenarioCliError);
});
test("polls bounded HTTP fixture readiness without reading response bodies", async () => {
let attempts = 0;
await waitForScenarioHttpReadiness({ url: "http://fixture.test/ready", expectedStatus: 204, timeoutMs: 1_000 }, new AbortController().signal, Date.now() + 1_000, async () => {
attempts += 1;
return new Response(null, { status: attempts < 2 ? 503 : 204 });
});
assert.equal(attempts, 2);
});
test("loads and validates with --check without starting a browser or provider", async () => {
await temporary(async (directory) => {
await writeFile(join(directory, "scenario.json"), scenario);
const result = await runScenarioCli(["--check", "--format", "json", "--cwd", directory, "scenario.json"]);
assert.equal(result.exitCode, 0);
assert.equal(result.report?.status, "validated");
assert.equal(result.loaded?.scenarios[0]?.scenario.id, "local-form");
});
});
test("rejects compose before hooks or browser startup when the CLI has no generation adapter", async () => {
await temporary(async (directory) => {
const marker = join(directory, "hook-ran");
await writeFile(join(directory, "scenario.json"), JSON.stringify({
version: 1,
id: "compose-unsupported",
settings: { textModel: "fixture/model" },
hooks: { setup: { argv: ["node", "-e", `require('node:fs').writeFileSync(${JSON.stringify(marker)}, 'ran')`] } },
operations: [{ op: "step", step: { instruction: "Greeting", action: "fill", compose: "compose a greeting" } }],
}));
let opened = false;
const manager: BrowserSessionManager = {
async open() { opened = true; throw new Error("browser must not start"); },
async openSetup() { throw new Error("setup is not used"); },
get() { throw new Error("browser must not start"); },
async close() {},
async closeAll() {},
};
const result = await runScenarioCli(["--cwd", directory, "scenario.json"], { sessionManager: manager });
assert.equal(result.exitCode, 2);
assert.match(result.report?.failure?.message ?? "", /no text-generation adapter/);
assert.equal(opened, false);
await assert.rejects(access(marker));
const checked = await runScenarioCli(["--check", "--cwd", directory, "scenario.json"], { sessionManager: manager });
assert.equal(checked.exitCode, 2);
assert.equal(opened, false);
await assert.rejects(access(marker));
});
});
test("fixture hook teardown terminates detached descendants through its owned process group", async (t) => {
if (process.platform === "win32") { t.skip("POSIX process-group supervision is not available"); return; }
await temporary(async (directory) => {
const pidPath = join(directory, "descendant.pid");
const hookScript = `const fs=require('node:fs'),cp=require('node:child_process');const child=cp.spawn(process.execPath,['-e','setTimeout(()=>{},100000)'],{stdio:'ignore'});fs.writeFileSync(${JSON.stringify(pidPath)},String(child.pid));child.unref()`;
await writeFile(join(directory, "scenario.json"), JSON.stringify({
version: 1,
id: "hook-group",
hooks: { setup: { argv: [process.execPath, "-e", hookScript], timeoutMs: 2_000 } },
settings: { allowedOrigins: ["http://fixture.test"], browser: { mode: "launch", product: "chromium", executablePath: "/definitely/missing/fixture-browser", startUrl: "http://fixture.test" } },
operations: [{ op: "read", request: {} }],
}));
const result = await runScenarioCli(["--cwd", directory, "scenario.json"]);
assert.equal(result.exitCode, 1);
const pid = Number(await readFile(pidPath, "utf8"));
await new Promise((resolve) => setTimeout(resolve, 100));
assert.throws(() => process.kill(pid, 0));
});
});
test("manages a published fixture server around setup, readiness, reset, browser, cleanup, and stop", async () => {
if (process.platform !== "linux") return;
await temporary(async (directory) => {
const probe = createServer();
await new Promise<void>((resolve, reject) => { probe.once("error", reject); probe.listen(0, "127.0.0.1", () => resolve()); });
const address = probe.address();
if (!address || typeof address === "string") throw new Error("could not allocate fixture test port");
const port = address.port;
await new Promise<void>((resolve, reject) => probe.close((error) => error ? reject(error) : resolve()));
const orderPath = join(directory, "order.log");
const append = (value: string): string => `require('node:fs').appendFileSync(${JSON.stringify(orderPath)},${JSON.stringify(`${value}\n`)})`;
const serverScript = `${append("server-start")};require('node:http').createServer((req,res)=>{res.writeHead(200);res.end('ready')}).listen(${port},'127.0.0.1')`;
const resetScript = `require('node:http').get('http://127.0.0.1:${port}/ready',r=>{r.resume();r.on('end',()=>${append("reset")})})`;
const cleanupScript = `require('node:http').get('http://127.0.0.1:${port}/ready',r=>{r.resume();r.on('end',()=>${append("cleanup")})})`;
await writeFile(join(directory, "scenario.json"), JSON.stringify({
version: 1,
id: "managed-fixture",
fixture: {
baseUrl: `http://127.0.0.1:${port}`,
server: {
mode: "managed",
start: { command: process.execPath, args: ["-e", serverScript], cwd: directory },
readiness: { url: `http://127.0.0.1:${port}/ready`, expectedStatus: 200, timeoutMs: 2_000 },
shutdownTimeoutMs: 2_000,
},
},
hooks: {
setup: { argv: [process.execPath, "-e", append("setup")] },
reset: { argv: [process.execPath, "-e", resetScript] },
cleanup: { argv: [process.execPath, "-e", cleanupScript] },
},
settings: { cdpUrl: "http://127.0.0.1:9222", allowedOrigins: [`http://127.0.0.1:${port}`], browser: { mode: "attach" } },
operations: [{ op: "read", request: {} }],
}));
const tab = { tabId: "tab-1" as BrowserTabId, kind: "page" as const, selected: true, owned: true as const, url: `http://127.0.0.1:${port}/ready`, origin: `http://127.0.0.1:${port}`, title: "Fixture" };
let runtimeClosed = false;
let sessionClosed = false;
let delayRead = false;
const runtime: BrowserRuntime = {
sessionId: "managed-session",
async observe() { return { observationId: "obs", url: tab.url, title: tab.title, candidates: [] }; },
async act() { return { verified: true, evidence: "ok" }; },
async read() { if (delayRead) await new Promise((resolve) => setTimeout(resolve, 1_000)); return { tab, documentId: "doc", url: tab.url, title: tab.title, visibleText: "ready", visibleTextTruncated: false }; },
async assert() { return { status: "satisfied", readOnly: true as const, tab, documentId: "doc" }; },
async navigate() { return { status: "committed", mutationState: "verified" as const, tab, documentId: "doc" }; },
async reload() { return { status: "committed", mutationState: "verified" as const, tab, documentId: "doc" }; },
async listTabs() { return [tab]; },
async selectTab() { return { selected: true as const, tab }; },
async waitPopup() { return { status: "timed_out", autoSwitched: false as const }; },
async closeTab() { return { closed: true as const, tabId: tab.tabId, mutationState: "verified" as const }; },
async captureEvidence() { return { status: "unavailable" as const, console: [], failedRequests: [] }; },
async narrowOriginPolicy() { return { allowedOrigins: [tab.origin] }; },
async close() { runtimeClosed = true; },
};
const session: BrowserSession = { id: "managed-session", config: { cdpUrl: "http://127.0.0.1:9222", allowedOrigins: [tab.origin], browser: { mode: "attach" } }, owned: false, signal: new AbortController().signal };
const manager: BrowserSessionManager = {
async open() { return session; },
async openSetup() { throw new Error("setup is not used"); },
get() { return session; },
async close() { sessionClosed = true; },
async closeAll() {},
};
const result = await runScenarioCli(["--cwd", directory, "scenario.json"], { sessionManager: manager, runtimeFactory: { create() { return runtime; } } });
assert.equal(result.exitCode, 0);
assert.equal(runtimeClosed, true);
assert.equal(sessionClosed, true);
assert.deepEqual((await readFile(orderPath, "utf8")).trim().split("\n"), ["setup", "server-start", "reset", "cleanup"]);
await assert.rejects(fetch(`http://127.0.0.1:${port}/ready`));
const earlyExitScript = `${append("server-start")};const server=require('node:http').createServer((req,res)=>{res.writeHead(200);res.end('ready')});server.listen(${port},'127.0.0.1');setTimeout(()=>process.exit(0),300)`;
delayRead = true;
await writeFile(join(directory, "scenario.json"), JSON.stringify({
version: 1,
id: "managed-early-exit",
fixture: {
baseUrl: `http://127.0.0.1:${port}`,
server: { mode: "managed", start: { command: process.execPath, args: ["-e", earlyExitScript], cwd: directory }, readiness: { url: `http://127.0.0.1:${port}/ready`, expectedStatus: 200, timeoutMs: 2_000 }, shutdownTimeoutMs: 2_000 },
},
hooks: { reset: { argv: [process.execPath, "-e", resetScript] }, cleanup: { argv: [process.execPath, "-e", append("cleanup")] } },
settings: { cdpUrl: "http://127.0.0.1:9222", allowedOrigins: [`http://127.0.0.1:${port}`], browser: { mode: "attach" } },
operations: [{ op: "read", request: {} }],
}));
const earlyExit = await runScenarioCli(["--cwd", directory, "scenario.json"], { sessionManager: manager, runtimeFactory: { create() { return runtime; } } });
assert.equal(earlyExit.exitCode, 1);
assert.equal(earlyExit.report?.scenarios[0]?.failure?.phase, "fixture");
delayRead = true;
await writeFile(join(directory, "scenario.json"), JSON.stringify({
version: 1,
id: "managed-external-abort",
fixture: {
baseUrl: `http://127.0.0.1:${port}`,
server: { mode: "managed", start: { command: process.execPath, args: ["-e", serverScript], cwd: directory }, readiness: { url: `http://127.0.0.1:${port}/ready`, expectedStatus: 200, timeoutMs: 2_000 }, shutdownTimeoutMs: 2_000 },
},
hooks: { reset: { argv: [process.execPath, "-e", resetScript] }, cleanup: { argv: [process.execPath, "-e", cleanupScript] } },
settings: { cdpUrl: "http://127.0.0.1:9222", allowedOrigins: [`http://127.0.0.1:${port}`], browser: { mode: "attach" } },
operations: [{ op: "read", request: {} }],
}));
const externalAbort = new AbortController();
const run = runScenarioCli(["--cwd", directory, "scenario.json"], { signal: externalAbort.signal, sessionManager: manager, runtimeFactory: { create() { return runtime; } } });
setTimeout(() => externalAbort.abort(new Error("test cancellation")), 500);
const aborted = await run;
assert.equal(aborted.exitCode, 1);
assert.equal((await readFile(orderPath, "utf8")).match(/^cleanup$/gm)?.length, 3);
await assert.rejects(fetch(`http://127.0.0.1:${port}/ready`));
const notReadyScript = `${append("server-start")};require('node:http').createServer((req,res)=>{res.writeHead(503);res.end('not ready')}).listen(${port},'127.0.0.1')`;
await writeFile(join(directory, "scenario.json"), JSON.stringify({
version: 1,
id: "managed-readiness-abort",
fixture: {
baseUrl: `http://127.0.0.1:${port}`,
server: { mode: "managed", start: { command: process.execPath, args: ["-e", notReadyScript], cwd: directory }, readiness: { url: `http://127.0.0.1:${port}/ready`, expectedStatus: 200, timeoutMs: 2_000 }, shutdownTimeoutMs: 2_000 },
},
hooks: { cleanup: { argv: [process.execPath, "-e", cleanupScript] } },
settings: { cdpUrl: "http://127.0.0.1:9222", allowedOrigins: [`http://127.0.0.1:${port}`], browser: { mode: "attach" } },
operations: [{ op: "read", request: {} }],
}));
const readinessAbort = new AbortController();
const readinessRun = runScenarioCli(["--cwd", directory, "scenario.json"], { signal: readinessAbort.signal, sessionManager: manager, runtimeFactory: { create() { return runtime; } } });
setTimeout(() => readinessAbort.abort(new Error("readiness cancellation")), 300);
const readinessAborted = await readinessRun;
assert.equal(readinessAborted.exitCode, 1);
assert.equal((await readFile(orderPath, "utf8")).match(/^cleanup$/gm)?.length, 4);
await assert.rejects(fetch(`http://127.0.0.1:${port}/ready`));
delayRead = false;
});
});
test("serializes attached unavailable and partial failure evidence without duplicate capture or unsafe details", async () => {
await temporary(async (directory) => {
const path = join(directory, "scenario.json");
await writeFile(path, JSON.stringify({
version: 1,
id: "attached-evidence",
settings: { cdpUrl: "http://127.0.0.1:9222", allowedOrigins: ["http://fixture.test"], browser: { mode: "attach" } },
operations: [{ op: "read", request: {} }],
}));
const tab = { tabId: "tab-evidence" as BrowserTabId, kind: "page" as const, selected: true, owned: true as const, url: "http://fixture.test/form", origin: "http://fixture.test", title: "Fixture" };
let mode: "unavailable" | "partial" = "unavailable";
let captureCalls = 0;
let closeError: Error | undefined;
const runtime: BrowserRuntime = {
sessionId: "session-evidence",
async observe() { return { observationId: "obs", url: tab.url, title: tab.title, candidates: [] }; },
async act() { return { verified: true, evidence: "ok" }; },
async read() {
const error = new BrowserOperationError({ code: "aborted", operation: "read", phase: "dispatch", message: "browser operation cancelled", mutationState: "not_attempted", retry: "never" });
const attached = mode === "unavailable"
? { status: "unavailable", reason: "opaque-token ws://user:password@example.test/private cookie=session-secret https://fixture.test/private?token=top-secret" }
: { status: "partial", reason: "partial capture", screenshot: { artifactId: "shot-1", kind: "screenshot", contentType: "image/png", path: "/tmp/private-shot.png", bytes: 4, owner: { sessionId: "session-evidence", runId: "run", operationId: "op" }, permissions: "0600", localOnly: true, modelForwarded: false } };
Object.defineProperty(error, "failureEvidence", { value: attached, enumerable: false });
throw error;
},
async assert() { return { status: "satisfied", readOnly: true as const, tab, documentId: "doc" }; },
async navigate() { return { status: "committed", mutationState: "verified" as const, tab, documentId: "doc" }; },
async reload() { return { status: "committed", mutationState: "verified" as const, tab, documentId: "doc" }; },
async listTabs() { return [tab]; },
async selectTab() { return { selected: true as const, tab }; },
async waitPopup() { return { status: "timed_out", autoSwitched: false as const }; },
async closeTab() { return { closed: true as const, tabId: tab.tabId, mutationState: "verified" as const }; },
async captureEvidence() { captureCalls += 1; return { status: "unavailable" as const, console: [], failedRequests: [] }; },
async narrowOriginPolicy() { return { allowedOrigins: [tab.origin] }; },
async close() {},
};
const session: BrowserSession = { id: "session-evidence", config: { cdpUrl: "http://127.0.0.1:9222", allowedOrigins: [tab.origin], browser: { mode: "attach" } }, owned: false, signal: new AbortController().signal };
const manager: BrowserSessionManager = { async open() { return session; }, async openSetup() { throw new Error("unused"); }, get() { return session; }, async close() { if (closeError) throw closeError; }, async closeAll() {} };
closeError = new Error("Browser session is no longer available: session closed");
const unavailable = await runScenarioCli(["--cwd", directory, "scenario.json"], { sessionManager: manager, runtimeFactory: { create() { return runtime; } } });
assert.equal(unavailable.exitCode, 1);
assert.equal(unavailable.report?.scenarios[0]?.cleanup?.status, "passed");
assert.equal(captureCalls, 0);
const unavailableEvidence = unavailable.report?.evidence.find((entry) => entry.status === "failed");
assert.equal(unavailableEvidence?.error, "failure evidence unavailable");
assert.doesNotMatch(JSON.stringify(unavailable.report), /opaque-token|password@example|session-secret|top-secret|private\?/);
mode = "partial";
closeError = new Error("browser profile cleanup failed");
const partial = await runScenarioCli(["--cwd", directory, "scenario.json"], { sessionManager: manager, runtimeFactory: { create() { return runtime; } } });
assert.equal(partial.exitCode, 1);
assert.equal(partial.report?.scenarios[0]?.cleanup?.status, "failed");
assert.equal(captureCalls, 0);
assert.equal(partial.report?.evidence.find((entry) => entry.status === "captured")?.retainedPath, "/tmp/private-shot.png");
assert.doesNotMatch(JSON.stringify(partial.report), /base64|top-secret/);
});
});
test("executes exact-name operations through an injected BrowserRuntime without browser or provider calls", async () => {
await temporary(async (directory) => {
const path = join(directory, "scenario.json");
await writeFile(path, JSON.stringify({
version: 1,
id: "offline-runtime",
settings: { cdpUrl: "http://127.0.0.1:9222", allowedOrigins: ["http://fixture.test"], browser: { mode: "attach" } },
operations: [
{ op: "step", step: { instruction: "Continue", action: "click", postcondition: { kind: "text", value: "done" } } },
{ op: "read", request: {} },
{ op: "listTabs", id: "tabs", request: { select: { kind: "origin", origin: "http://fixture.test" } } },
{ op: "selectTab", id: "selected", request: { tab: { source: "operation", operation: { id: "tabs" }, result: "listedTab", index: 0 } } },
{ op: "read", request: { tab: { source: "operation", operation: { id: "selected" }, result: "selectedTab" } } },
{ op: "listTabs", id: "all" },
{ op: "selectTab", id: "other", request: { tab: { source: "operation", operation: { id: "all" }, result: "listedTab", index: 1 } } },
{ op: "read", request: { tab: { source: "initial", target: "selected" } } },
],
evidence: { onFailure: true, onRequest: true },
}));
const tab = { tabId: "tab-1" as BrowserTabId, kind: "page" as const, selected: true, owned: true as const, url: "http://fixture.test/form.html", origin: "http://fixture.test", title: "Fixture" };
const otherTab = { tabId: "tab-2" as BrowserTabId, kind: "page" as const, selected: false, owned: true as const, url: "http://other.test/form.html", origin: "http://other.test", title: "Other" };
let currentSelected = tab.tabId;
let closed = false;
let delayAct = false;
let artifactStore: PrivateArtifactStore | undefined;
const runtime: BrowserRuntime = {
sessionId: "session-1",
async observe() { return { observationId: "obs-1", url: tab.url, title: tab.title, candidates: [{ id: "continue", role: "button", name: "Continue", actions: ["click"] }] }; },
async act(_request, signal) {
if (delayAct) await new Promise((resolve) => setTimeout(resolve, 100));
signal.throwIfAborted();
return { verified: true, evidence: "visible-text postcondition verified" };
},
async read() { return { tab, documentId: "doc-1", url: tab.url, title: tab.title, visibleText: "Fixture", visibleTextTruncated: false }; },
async assert() { return { status: "satisfied", readOnly: true as const, tab, documentId: "doc-1" }; },
async navigate() { return { status: "committed", mutationState: "verified" as const, tab, documentId: "doc-2" }; },
async reload() { return { status: "committed", mutationState: "verified" as const, tab, documentId: "doc-2" }; },
async listTabs() { return [{ ...tab, selected: currentSelected === tab.tabId }, { ...otherTab, selected: currentSelected === otherTab.tabId }]; },
async selectTab(request) { currentSelected = request.tabId; return { selected: true as const, tab: request.tabId === otherTab.tabId ? otherTab : tab }; },
async waitPopup() { return { status: "timed_out", autoSwitched: false as const }; },
async closeTab() { return { closed: true as const, tabId: tab.tabId, mutationState: "verified" as const }; },
async captureEvidence() {
const writer = await artifactStore!.open({ sessionId: "session-1", runId: "run-1", operationId: "op-1" }, "diagnostics");
await writer.write(new TextEncoder().encode("{}"));
const metadata = await writer.commit();
return { status: "complete" as const, diagnostics: metadata, console: [], failedRequests: [] };
},
async narrowOriginPolicy() { return { allowedOrigins: ["http://fixture.test"] }; },
async close() { closed = true; await artifactStore?.close(); },
};
const session: BrowserSession = { id: "session-1", config: { cdpUrl: "http://127.0.0.1:9222", allowedOrigins: ["http://fixture.test"], browser: { mode: "attach" } }, owned: false, signal: new AbortController().signal };
const manager: BrowserSessionManager = {
async open() { return session; },
async openSetup() { throw new Error("setup is not used"); },
get() { return session; },
async close() {},
async closeAll() {},
};
const reportPath = join(directory, "report.json");
const result = await runScenarioCli(["--cwd", directory, "--report", reportPath, "scenario.json"], {
sessionManager: manager,
runtimeFactory: { create(options) { artifactStore = options.artifacts; return runtime; } },
});
assert.equal(result.exitCode, 0);
assert.equal(result.report?.status, "passed");
assert.deepEqual(result.report?.scenarios[0]?.operations?.map((operation) => operation.status), ["passed", "passed", "passed", "passed", "passed", "passed", "passed", "passed"]);
assert.equal(closed, true);
const retainedPath = result.report?.evidence.find((item) => item.status === "captured")?.retainedPath;
assert.ok(retainedPath);
await access(retainedPath);
const saved = JSON.parse(await readFile(reportPath, "utf8")) as { evidence?: Array<{ status?: string; file?: string; retainedPath?: string }> };
const savedEvidence = saved.evidence?.find((item) => item.status === "captured");
const savedPath = savedEvidence?.retainedPath;
assert.equal(savedPath, retainedPath);
assert.equal(savedEvidence?.file, retainedPath);
await access(savedPath!);
assert.match(result.report?.scenarios[0]?.cleanup?.detail ?? "", /committed private evidence retained/);
delayAct = true;
await writeFile(path, JSON.stringify({
version: 1,
id: "timeout",
settings: { cdpUrl: "http://127.0.0.1:9222", allowedOrigins: ["http://fixture.test"], browser: { mode: "attach" } },
budgets: { timeoutMs: 20 },
operations: [
{ op: "step", step: { instruction: "Continue", action: "click", postcondition: { kind: "text", value: "done" } } },
{ op: "step", step: { instruction: "Continue", action: "click", postcondition: { kind: "text", value: "done" } } },
],
}));
const timedOut = await runScenarioCli(["--cwd", directory, "scenario.json"], {
sessionManager: manager,
runtimeFactory: { create(options) { artifactStore = options.artifacts; return runtime; } },
});
assert.equal(timedOut.exitCode, 1);
assert.equal(timedOut.report?.scenarios[0]?.operations?.length, 1);
assert.equal(timedOut.report?.scenarios[0]?.operations?.[0]?.status, "failed");
});
});
test("executes through the BrowserRuntime seam and reports startup failure safely", async () => {
await temporary(async (directory) => {
await writeFile(join(directory, "scenario.json"), scenario);
const reportPath = join(directory, "report.json");
const result = await runScenarioCli(["--cwd", directory, "--report", reportPath, "scenario.json"]);
assert.equal(result.exitCode, 1);
assert.equal(result.report?.status, "failed");
assert.match(result.report?.failure?.message ?? "", /Browser process failed to start/);
assert.equal(JSON.parse(await readFile(reportPath, "utf8")).version, 1);
});
});
test("reports malformed input without echoing file contents", async () => {
await temporary(async (directory) => {
await writeFile(join(directory, "bad.json"), "{\"password\":\"private-secret\"");
const result = await runScenarioCli(["--cwd", directory, "bad.json"]);
assert.equal(result.exitCode, 2);
assert.doesNotMatch(JSON.stringify(result.report), /private-secret/);
});
});