diff --git a/scripts/lib/browser-source-map-e2e.mjs b/scripts/lib/browser-source-map-e2e.mjs index 8f9669d40a..504a981b7b 100644 --- a/scripts/lib/browser-source-map-e2e.mjs +++ b/scripts/lib/browser-source-map-e2e.mjs @@ -1,18 +1,23 @@ import assert from "node:assert/strict"; -import { execFile } from "node:child_process"; import { createHash } from "node:crypto"; -import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; -import { tmpdir } from "node:os"; +import { readFile, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; -import { promisify } from "node:util"; import { Client } from "@modelcontextprotocol/client"; import { StdioClientTransport } from "@modelcontextprotocol/client/stdio"; import { parseEvidence } from "../../dist/domain/evidence.js"; import { webSourceLocationResultSchema } from "../../dist/domain/webSourceLocation.js"; +import { PrivateRuntimeRoot } from "../../dist/process/PrivateRuntimeRoot.js"; import { startBrowserSourceMapSite } from "../fixtures/browser-source-map-site.mjs"; import { mcpTextValue } from "./mcp-verifier-results.mjs"; import { withVerifierCleanup } from "./verifier-cleanup.mjs"; +import { createPackageVerifierCommands } from "./verify-package-commands.mjs"; +import { importVerifierSource } from "./verifier-source-loader.mjs"; + +const { BROWSER_DEVTOOLS_STARTUP_TIMEOUT_MS } = await importVerifierSource( + new URL("./browser-process-startup.mts", import.meta.url).href, + import.meta.url, +); /** Verify real Chrome capture → export → original compiler point through packaged CLI/MCP. */ export async function verifyBrowserSourceMap( @@ -21,21 +26,35 @@ export async function verifyBrowserSourceMap( ) { let site; let root; + let commands; let transport; let client; + const closeResource = async (owner, resource) => { + if (owner === undefined) return; + try { + await owner.close(); + } catch (cause) { + commands?.adopt(owner, resource); + throw cause; + } + }; return withVerifierCleanup(async () => { site = await startBrowserSourceMapSite(); - root = await mkdtemp(join(tmpdir(), "rea-browser-source-map-")); + root = await PrivateRuntimeRoot.create({ + prefix: "rea-browser-source-map-", + }); + commands = await createPackageVerifierCommands({ workspace: root }); const env = { ...process.env, REA_LOG_LEVEL: "silent", HOPPER_LAUNCHER_PATH: "/rea-unconfigured-provider/hopper", }; const cli = async (...args) => { - const { stdout } = await promisify(execFile)( + const { stdout } = await commands.exec( process.execPath, [entrypoint, ...args, "--json"], - { env, timeout: 45000, maxBuffer: 16 * 1024 * 1024 }, + // Allow the shared cold-start budget plus capture, cleanup and delivery. + { env, timeout: 120_000, maxBuffer: 16 * 1024 * 1024 }, ); return parseEvidence(JSON.parse(stdout)); }; @@ -50,6 +69,7 @@ export async function verifyBrowserSourceMap( "capture-browser-scenario", JSON.stringify({ browser: { mode: "launch", executable_path: executable }, + command_timeout_ms: BROWSER_DEVTOOLS_STARTUP_TIMEOUT_MS, start_url: { url: site.origin }, actions: [ { @@ -66,12 +86,12 @@ export async function verifyBrowserSourceMap( capture: { network: { response_body: true } }, }), ); - const capturePath = join(root, "capture.json"); + const capturePath = join(root.path, "capture.json"); await writeFile(capturePath, JSON.stringify(capture)); const exported = await cli( "export-web-scripts", capturePath, - join(root, "export"), + join(root.path, "export"), ); const manifest = exported.normalized_result; const manifestBytes = await readFile(manifest.manifest.path); @@ -86,7 +106,7 @@ export async function verifyBrowserSourceMap( scriptIndex >= 0, "Actual browser capture omitted compiled source", ); - const mapPath = join(root, "app.js.map"); + const mapPath = join(root.path, "app.js.map"); await writeFile(mapPath, site.map); const before = site.mapRequests(); await client.connect(transport); @@ -132,13 +152,11 @@ export async function verifyBrowserSourceMap( public_point_cases: 4, }; }, [ - () => client?.close(), - () => transport?.close(), - () => site?.close(), - () => - root === undefined - ? undefined - : rm(root, { recursive: true, force: true }), + () => closeResource(client, "source-map MCP client"), + () => closeResource(transport, "source-map stdio transport"), + () => closeResource(site, "source-map fixture site"), + () => commands?.close(), + () => (commands === undefined ? root?.close() : undefined), ]); } function assertPoint(evidence, { site, input, capture, identity }) { diff --git a/tests/boundary/process/browserRequestVerifierOwnership.test.ts b/tests/boundary/process/browserRequestVerifierOwnership.test.ts index 12bf0c9136..a51805cbb2 100644 --- a/tests/boundary/process/browserRequestVerifierOwnership.test.ts +++ b/tests/boundary/process/browserRequestVerifierOwnership.test.ts @@ -13,7 +13,7 @@ import { processTest } from "../../support/process/processFixture.js"; // Records once if the lane's root disappears while this stand-in browser still runs. const browserSource = ` import { appendFileSync, existsSync, readdirSync } from "node:fs"; -const rootExists = () => readdirSync(process.env.OWNER_TMP).some((name) => name.startsWith("rea-request-e2e-")); +const rootExists = () => readdirSync(process.env.OWNER_TMP).some((name) => name.startsWith(process.env.OWNER_ROOT_PREFIX)); let seen = false; setInterval(() => { if (existsSync(process.env.OWNER_RELEASE)) process.exit(0); @@ -77,82 +77,105 @@ const releaseBrowser = async (release: string, ready: string) => { // The stand-in CLI and browser are authored; the lane, its owner, the private root // and process cleanup are real. No browser capture is simulated or claimed. -processTest.skipIf(process.platform === "win32")( - "stops a failed capture's CLI and the browser it launched before removing the request lane's root", - { timeout: 40_000 }, - async ({ processes }) => { - const root = await createTestTempDirectory("rea-browser-request-owner-"); - const tmp = join(root, "tmp"); - await mkdir(tmp); - const cli = join(root, "cli.mjs"); - const browser = join(root, "browser.mjs"); - const ready = join(root, "ready.json"); - const release = join(root, "release-browser"); - const events = join(root, "events.jsonl"); - const driver = join(root, "driver.mjs"); - await writeFile(cli, cliSource); - await writeFile(browser, browserSource); - const lane = pathToFileURL( - resolve("scripts/lib/browser-request-e2e.mjs"), - ).href; - const observations = pathToFileURL( - resolve("dist/process/ProcessOwnershipObservation.js"), - ).href; - const tree = pathToFileURL( - resolve("dist/process/ProcessOwnershipProcessTree.js"), - ).href; - await writeFile( - driver, - ` +for (const { name, module, verify, rootPrefix } of [ + { + name: "request", + module: "browser-request-e2e.mjs", + verify: "verifyBrowserRequestSteps", + rootPrefix: "rea-request-e2e-", + }, + { + name: "source-map", + module: "browser-source-map-e2e.mjs", + verify: "verifyBrowserSourceMap", + rootPrefix: "rea-browser-source-map-", + }, +]) { + processTest.skipIf(process.platform === "win32")( + `stops a failed capture's CLI and browser before removing the ${name} lane's root`, + { timeout: 40_000 }, + async ({ processes }) => { + const root = await createTestTempDirectory(`rea-browser-${name}-owner-`); + const tmp = join(root, "tmp"); + await mkdir(tmp); + const cli = join(root, "cli.mjs"); + const browser = join(root, "browser.mjs"); + const ready = join(root, "ready.json"); + const release = join(root, "release-browser"); + const events = join(root, "events.jsonl"); + const driver = join(root, "driver.mjs"); + const compiler = join(root, "compiler.mjs"); + // This lifecycle probe stops at CLI failure, before browser or map proof. + await writeFile( + compiler, + 'export const version = "0.25.10"; export const transform = async (code) => ({ code, map: "{}" }); export const stop = () => {};', + ); + await writeFile(cli, cliSource); + await writeFile(browser, browserSource); + const lane = pathToFileURL(resolve("scripts/lib", module)).href; + const observations = pathToFileURL( + resolve("dist/process/ProcessOwnershipObservation.js"), + ).href; + const tree = pathToFileURL( + resolve("dist/process/ProcessOwnershipProcessTree.js"), + ).href; + await writeFile( + driver, + ` import { readFile, readdir } from "node:fs/promises"; -import { verifyBrowserRequestSteps } from ${JSON.stringify(lane)}; +import { ${verify} } from ${JSON.stringify(lane)}; import { systemProcessOwnershipHost } from ${JSON.stringify(observations)}; import { liveProcesses } from ${JSON.stringify(tree)}; let failure; try { - await verifyBrowserRequestSteps("/rea-unlaunched-browser", process.env.OWNER_CLI); + await ${verify}("/rea-unlaunched-browser", process.env.OWNER_CLI); } catch (cause) { failure = { name: cause?.name ?? typeof cause, message: cause?.message ?? String(cause) }; } const { pids } = JSON.parse(await readFile(process.env.OWNER_READY, "utf8")); const live = liveProcesses(await systemProcessOwnershipHost.listProcesses()).filter(({ pid }) => pids.includes(pid)).map(({ pid }) => pid); -const roots = (await readdir(process.env.OWNER_TMP)).filter((name) => name.startsWith("rea-request-e2e-")); +const roots = (await readdir(process.env.OWNER_TMP)).filter((name) => name.startsWith(process.env.OWNER_ROOT_PREFIX)); process.stdout.write(JSON.stringify({ pids, live, roots, failure })); `, - ); - try { - const result = await processes.run(process.execPath, [driver], { - timeoutMs: 30_000, - env: { - ...process.env, - TMPDIR: tmp, - OWNER_TMP: tmp, - OWNER_CLI: cli, - OWNER_BROWSER_ENTRY: browser, - OWNER_READY: ready, - OWNER_RELEASE: release, - OWNER_EVENTS: events, - }, - }); - expect(result.exitCode, result.stderr).toBe(0); - const report = reportSchema.parse(JSON.parse(result.stdout)); - expect(report.live).toEqual([]); - const receipts = await readFile(events, "utf8").catch( - (cause: unknown) => { - if ( - cause instanceof Error && - "code" in cause && - cause.code === "ENOENT" - ) - return ""; - throw cause; - }, ); - expect(receipts).toBe(""); - expect(report.roots).toEqual([]); - expect(report.failure.message).toMatch(/output exceeded/u); - } finally { - await releaseBrowser(release, ready); - } - }, -); + try { + const result = await processes.run(process.execPath, [driver], { + timeoutMs: 30_000, + env: { + ...process.env, + TMPDIR: tmp, + OWNER_TMP: tmp, + OWNER_ROOT_PREFIX: rootPrefix, + REA_WEB_SOURCE_MAP_COMPILER: compiler, + OWNER_CLI: cli, + OWNER_BROWSER_ENTRY: browser, + OWNER_READY: ready, + OWNER_RELEASE: release, + OWNER_EVENTS: events, + }, + }); + expect(result.exitCode, result.stderr).toBe(0); + const report = reportSchema.parse(JSON.parse(result.stdout)); + expect(report.live).toEqual([]); + const receipts = await readFile(events, "utf8").catch( + (cause: unknown) => { + if ( + cause instanceof Error && + "code" in cause && + cause.code === "ENOENT" + ) + return ""; + throw cause; + }, + ); + expect(receipts).toBe(""); + expect(report.roots).toEqual([]); + expect(report.failure.message).toMatch( + /output exceeded|maxBuffer length exceeded/u, + ); + } finally { + await releaseBrowser(release, ready); + } + }, + ); +}