mirror of
https://github.com/morluto/rea.git
synced 2026-11-05 14:26:07 +00:00
fix(ci): own source-map captures and honor browser startup budget
This commit is contained in:
1 parent
a1660bc016
commit
9031adf0b0
2 files changed
+125
-84
No files matched your search
@@ -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 }) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user