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): Promise { 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((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((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/); }); });