fix(ci): allow cold Chrome startup in runtime fixtures

Let the fixture lifecycle own CDP readiness with the same 60-second startup deadline as the broader browser verifier. Return the discovered port to both consumers and clean up failed startup before returning a lease.

Validation: five startup/cleanup boundary tests, npm run check, and real Chrome 154 runtime and source-map CLI/MCP verifiers passed on macOS arm64. Hosted Linux verification follows the push.
This commit is contained in:
morluto
2026-10-13 02:17:14 +08:00
parent ec2d251c0a
commit f941968eb1
4 changed files with 61 additions and 32 deletions
+1 -14
View File
@@ -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,
+1 -15
View File
@@ -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],
@@ -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, {
@@ -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(