diff --git a/scripts/lib/browser-nested-source-map-e2e.mjs b/scripts/lib/browser-nested-source-map-e2e.mjs index 0325b69e48..ff2af88632 100644 --- a/scripts/lib/browser-nested-source-map-e2e.mjs +++ b/scripts/lib/browser-nested-source-map-e2e.mjs @@ -1,13 +1,11 @@ import assert from "node:assert/strict"; import { execFile } from "node:child_process"; import { createHash } from "node:crypto"; -import { join } from "node:path"; import { fileURLToPath } from "node:url"; import { promisify } from "node:util"; import { TraceMap, eachMapping } from "@jridgewell/trace-mapping"; import { CdpBrowserProvider } from "../../dist/browser/CdpBrowserProvider.js"; import { CdpConnection } from "../../dist/browser/CdpConnection.js"; -import { waitForBrowserDevtoolsPort } from "../../dist/browser/BrowserProcessStartup.js"; import { parseEvidence } from "../../dist/domain/evidence.js"; import { traceSourceMap } from "../../dist/javascript/sourceMaps/TraceSourceMap.js"; import { startBrowserSourceMapSite } from "../fixtures/browser-source-map-site.mjs"; @@ -63,7 +61,6 @@ async function verifyCase(executable, entrypoint, scenario) { let fixture; let connection; let primaryError; - let stderr = ""; let phase = "native-map-admission"; let browserVersion; let expected; @@ -90,17 +87,7 @@ async function verifyCase(executable, entrypoint, scenario) { phase = "owned-browser-startup"; fixture = await startRuntimeFixtureBrowser(executable, site.origin); - fixture.browser.stderr.on("data", (chunk) => { - if (stderr.length < 65536) stderr += chunk; - }); - const port = await waitForBrowserDevtoolsPort({ - child: fixture.browser, - executable, - activePortPath: join(fixture.profile, "DevToolsActivePort"), - stderr: () => stderr, - timeoutMs: 20_000, - }); - const endpoint = `http://127.0.0.1:${port}`; + const endpoint = `http://127.0.0.1:${fixture.port}`; const provider = new CdpBrowserProvider(); const targets = await provider.listTargets({ cdp_endpoint: endpoint, diff --git a/scripts/lib/browser-runtime-e2e.mjs b/scripts/lib/browser-runtime-e2e.mjs index c2ebbed021..c4cb4f09f6 100644 --- a/scripts/lib/browser-runtime-e2e.mjs +++ b/scripts/lib/browser-runtime-e2e.mjs @@ -3,14 +3,12 @@ import { spawn, execFile } from "node:child_process"; import { createHash } from "node:crypto"; import { once } from "node:events"; import { access } 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 { CdpBrowserProvider } from "../../dist/browser/CdpBrowserProvider.js"; import { CdpConnection } from "../../dist/browser/CdpConnection.js"; -import { waitForBrowserDevtoolsPort } from "../../dist/browser/BrowserProcessStartup.js"; import { parseEvidence } from "../../dist/domain/evidence.js"; import { webExecutionSchema } from "../../dist/domain/webExecution.js"; import { webEventListenersSchema } from "../../dist/domain/webEventListeners.js"; @@ -42,23 +40,11 @@ export async function verifyBrowserRuntime( const site = await startBrowserRuntimeSite(); let fixture; let primaryError; - let stderr = ""; let action; let client; try { fixture = await startRuntimeFixtureBrowser(executable, site.origin); - const { browser, profile } = fixture; - browser.stderr.on("data", (chunk) => { - if (stderr.length < 65536) stderr += chunk; - }); - const port = await waitForBrowserDevtoolsPort({ - child: browser, - executable, - activePortPath: join(profile, "DevToolsActivePort"), - stderr: () => stderr, - timeoutMs: 20_000, - }); - const endpoint = `http://127.0.0.1:${port}`; + const endpoint = `http://127.0.0.1:${fixture.port}`; const targets = await new CdpBrowserProvider().listTargets({ cdp_endpoint: endpoint, allowed_origins: [site.origin], diff --git a/scripts/lib/browser-runtime-fixture-lifecycle.mjs b/scripts/lib/browser-runtime-fixture-lifecycle.mjs index 24b88c793e..d39617f58c 100644 --- a/scripts/lib/browser-runtime-fixture-lifecycle.mjs +++ b/scripts/lib/browser-runtime-fixture-lifecycle.mjs @@ -8,8 +8,9 @@ import { spawnOwnedProviderProcess } from "../../dist/process/ProviderProcess.js import { createSystemProcessOwnershipHost } from "../../dist/process/ProcessOwnershipObservation.js"; import { liveProcesses } from "../../dist/process/ProcessOwnershipProcessTree.js"; import { CdpConnection } from "../../dist/browser/CdpConnection.js"; +import { waitForBrowserDevtoolsPort } from "../../dist/browser/BrowserProcessStartup.js"; -/** Launch only the synthetic fixture browser in its own verified process group. */ +/** Launch the synthetic fixture browser and await CDP in its own verified process group. */ export async function startRuntimeFixtureBrowser(executable, origin) { const profile = await mkdtemp(join(tmpdir(), "rea-real-web-runtime-")); let owned; @@ -38,9 +39,21 @@ export async function startRuntimeFixtureBrowser(executable, origin) { expectedCommand: null, }); owned.process.stdout?.resume(); + let stderr = ""; + owned.process.stderr?.on("data", (chunk) => { + if (stderr.length < 65536) stderr += chunk; + }); + const port = await waitForBrowserDevtoolsPort({ + child: owned.process, + executable, + activePortPath: join(profile, "DevToolsActivePort"), + stderr: () => stderr, + // Match the broader real-browser verifier's allowance for cold Chrome + // startup on shared runners; early process failures still reject at once. + timeoutMs: 60_000, + }); return { - browser: owned.process, - profile, + port, close: async () => { await stopOwnedBrowser(owned, profile); await rm(profile, { diff --git a/tests/boundary/browser/runtimeFixtureCleanup.test.ts b/tests/boundary/browser/runtimeFixtureCleanup.test.ts index be6aca7cff..4aafa317f9 100644 --- a/tests/boundary/browser/runtimeFixtureCleanup.test.ts +++ b/tests/boundary/browser/runtimeFixtureCleanup.test.ts @@ -1,6 +1,49 @@ import { execFile } from "node:child_process"; +import { readdir } from "node:fs/promises"; import { promisify } from "node:util"; import { expect, it } from "vitest"; +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; + +it.skipIf(process.platform === "win32")( + "rejects an exited fixture before returning a lease and removes its profile", + async () => { + const root = await createTestTempDirectory("rea-runtime-startup-failure-"); + const helper = new URL( + "../../../scripts/lib/browser-runtime-fixture-lifecycle.mjs", + import.meta.url, + ); + const script = ` + const { startRuntimeFixtureBrowser } = await import(${JSON.stringify(helper.href)}); + // Node rejects Chrome's arguments immediately, exercising actual process exit. + try { + const fixture = await startRuntimeFixtureBrowser(process.execPath, 'http://127.0.0.1'); + await fixture.close(); + throw new Error('Expected startup failure'); + } catch (error) { + if (error._tag !== 'BrowserStartupError') throw error; + console.log(JSON.stringify({ + failure: error.failure, + exitCode: error.exitCode, + stderr: error.stderr, + })); + } + `; + const { stdout } = await promisify(execFile)( + process.execPath, + ["--input-type=module", "-e", script], + { + env: { ...process.env, TMPDIR: root, TMP: root, TEMP: root }, + timeout: 10_000, + }, + ); + expect(JSON.parse(stdout)).toEqual({ + failure: "exited", + exitCode: expect.any(Number), + stderr: expect.stringContaining("bad option"), + }); + expect(await readdir(root)).toEqual([]); + }, +); it("attempts every fixture cleanup and retains the primary capture failure", async () => { const helper = new URL(