Files
rea/scripts/lib/browser-module-e2e.mjs
T
ysopwnable 01be082e9e feat(browser): trace captured website module imports (#879)
* feat(browser): trace captured website module imports

Compose verified exported scripts, inert native import syntax and a narrow
Chromium resolution port. Preserve query/fragment identity, import-map context,
all reported capture candidates and explicit unknown execution. Share named
CLI/MCP contracts and retain native reports, diagnostics and cleanup failures.

Add focused boundary regressions and a real module-loading oracle to the
existing conditional Chrome lane. No captured application code is executed
by module tracing, and no selected website asset is refetched.

Refs #851, #774

Assisted-by: Yukari (gensokyo-skills:yukari-boundary-analysis)
Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing)

* fix(browser): preserve module contexts and source bindings

Keep capture completeness inline, validate actual source text against its
digest, and separate unsupported URL contexts from malformed input.
Use the importer origin for native module resolution and the selected map
base independently, including mixed HTTP/HTTPS and escaped HTML attributes.

Refs #851, #774

Assisted-by: Yukari (gensokyo-skills:yukari-boundary-analysis)
Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing)
2026-10-07 14:59:52 +08:00

391 lines
13 KiB
JavaScript

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 { join } from "node:path";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
import { chromium } from "playwright-core";
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { parseEvidence } from "../../dist/domain/evidence.js";
import { webModuleTraceResultSchema } from "../../dist/domain/webModuleTrace.js";
import { NativeModuleResolver } from "../../dist/browser/modules/NativeModuleResolver.js";
import { startBrowserModuleSite } from "../fixtures/browser-module-site.mjs";
import { mcpTextValue } from "./mcp-verifier-results.mjs";
/** Check native URL resolution against an independently loaded module graph through CLI/MCP. */
export async function verifyBrowserModules(
executable,
entrypoint = fileURLToPath(new URL("../rea.mjs", import.meta.url)),
) {
const site = await startBrowserModuleSite();
const root = await mkdtemp(join(tmpdir(), "rea-browser-modules-"));
const env = {
...process.env,
REA_LOG_LEVEL: "silent",
REA_BROWSER_EXECUTABLE: executable,
HOPPER_LAUNCHER_PATH: "/rea-unconfigured-provider/hopper",
};
const cli = async (...args) => {
const { stdout } = await promisify(execFile)(
process.execPath,
[entrypoint, ...args, "--json"],
{ env, timeout: 45000, maxBuffer: 16 * 1024 * 1024 },
);
return parseEvidence(JSON.parse(stdout));
};
const transport = new StdioClientTransport({
command: process.execPath,
args: [entrypoint, "mcp"],
env,
stderr: "pipe",
});
const client = new Client({ name: "module-real-e2e", version: "1" });
try {
const oracle = await loadingOracle(executable, site);
const scenario = {
browser: { mode: "launch", executable_path: executable },
start_url: { url: site.origin },
actions: [
{
step_id: "ready",
action: "wait_for",
locator: { kind: "css", selector: '[data-ready="true"]' },
state: "visible",
timeout_ms: 10000,
},
],
capture: { network: { response_body: true } },
};
const capture = await cli(
"capture-browser-scenario",
JSON.stringify(scenario),
);
const capturePath = join(root, "capture.json");
await writeFile(capturePath, JSON.stringify(capture));
const exported = await cli(
"export-web-scripts",
capturePath,
join(root, "export"),
);
const manifest = exported.normalized_result;
const scriptIndex = manifest.scripts.findIndex(
(script) => script.url === `${site.origin}/scoped/main.js?build=2`,
);
assert.ok(
scriptIndex >= 0,
"Real capture did not retain main module response",
);
const mapPath = join(root, "import-map.json");
await writeFile(mapPath, JSON.stringify(site.importMap));
const input = {
manifest_path: manifest.manifest.path,
script_index: scriptIndex,
importer_url: `${site.origin}/scoped/main.js?build=2#entry`,
import_map: { path: mapPath, base_url: `${site.origin}/maps/` },
};
const before = [...site.requests];
const cliEvidence = await cli(
"trace-web-module-imports",
input.manifest_path,
String(scriptIndex),
"--importer-url",
input.importer_url,
"--import-map-path",
mapPath,
"--import-map-base-url",
input.import_map.base_url,
);
const cliResult = assertTrace(cliEvidence, oracle, site, scriptIndex);
await client.connect(transport);
const response = await client.callTool({
name: "trace_web_module_imports",
arguments: input,
});
assert.notEqual(response.isError, true, mcpTextValue(response));
const mcpEvidence = parseEvidence(
JSON.parse(mcpTextValue(response)).evidence,
);
const mcpResult = assertTrace(mcpEvidence, oracle, site, scriptIndex);
assert.deepEqual(mcpResult, cliResult, "CLI and MCP module traces differ");
assert.equal(
cliEvidence.subject.digest.sha256,
createHash("sha256").update(site.main).digest("hex"),
);
const mapBytes = await readFile(mapPath);
assert.equal(
cliResult.import_map.sha256,
createHash("sha256").update(mapBytes).digest("hex"),
);
assert.deepEqual(
site.requests,
before,
"Tracing refetched or executed a captured website dependency",
);
for (const surface of ["cli", "stdio_mcp"]) {
const withoutMap =
surface === "cli"
? await cli(
"trace-web-module-imports",
input.manifest_path,
String(scriptIndex),
)
: parseEvidence(
JSON.parse(
mcpTextValue(
await client.callTool({
name: "trace_web_module_imports",
arguments: {
manifest_path: input.manifest_path,
script_index: scriptIndex,
},
}),
),
).evidence,
);
const result = webModuleTraceResultSchema.parse(
withoutMap.normalized_result,
);
assert.equal(result.import_map, null);
assert.equal(result.imports[0].resolution.state, "rejected");
assert.equal(
result.imports[2].resolution.url,
`${site.origin}/query.js?v=1#one`,
);
}
assert.deepEqual(
site.requests,
before,
"Map-free tracing refetched an asset",
);
const warnings = await new NativeModuleResolver(env).resolve({
importerUrl: input.importer_url,
specifiers: ["alias"],
importMap: {
baseUrl: input.import_map.base_url,
value: { imports: { alias: "./global.js", ignored: 42 } },
},
});
assert.equal(warnings.ok, true, warnings.ok ? "" : warnings.error.message);
assert.ok(
warnings.value.diagnostics.length > 0,
"Native import-map warnings were discarded",
);
assert.equal(
warnings.value.resolutions[0].url,
`${site.origin}/maps/global.js`,
);
const controller = new AbortController();
let owned;
const interrupted = await new NativeModuleResolver(env, async (options) => {
owned = await chromium.launch(options);
controller.abort();
return owned;
}).resolve(
{
importerUrl: input.importer_url,
importMap: null,
specifiers: ["./dep.js"],
},
{ signal: controller.signal },
);
assert.equal(
interrupted.ok,
false,
"Cancellation returned successful native resolution",
);
assert.equal(interrupted.error._tag, "AnalysisCancelledError");
assert.equal(
owned.isConnected(),
false,
"Cancelled native resolver left its owned browser connected",
);
const syntheticImporter = "http://app.invalid/main.js?build=1#entry";
const syntheticMapBase = "https://maps.invalid/maps/map.json?x=1&y=2#base";
for (const useMap of [false, true])
for (const surface of ["cli", "stdio_mcp"]) {
let evidence;
if (surface === "cli")
evidence = await cli(
"trace-web-module-imports",
input.manifest_path,
String(scriptIndex),
"--importer-url",
syntheticImporter,
...(useMap
? [
"--import-map-path",
mapPath,
"--import-map-base-url",
syntheticMapBase,
]
: []),
);
else {
const response = await client.callTool({
name: "trace_web_module_imports",
arguments: {
manifest_path: input.manifest_path,
script_index: scriptIndex,
importer_url: syntheticImporter,
...(useMap
? { import_map: { path: mapPath, base_url: syntheticMapBase } }
: {}),
},
});
assert.notEqual(response.isError, true, mcpTextValue(response));
evidence = parseEvidence(JSON.parse(mcpTextValue(response)).evidence);
}
const result = webModuleTraceResultSchema.parse(
evidence.normalized_result,
);
if (useMap)
assert.deepEqual(result.imports[0].resolution, {
state: "resolved",
url: "https://maps.invalid/maps/global.js",
});
else assert.equal(result.imports[0].resolution.state, "rejected");
assert.equal(
result.imports[2].resolution.url,
"http://app.invalid/query.js?v=1#one",
);
assert.equal(
evidence.raw_result.native_report.importer_url,
syntheticImporter,
);
assert.equal(
result.source.script.url,
`${site.origin}/scoped/main.js?build=2`,
);
}
const escapedBase = await new NativeModuleResolver(env).resolve({
importerUrl: syntheticImporter,
importMap: {
baseUrl: "https://maps.invalid/maps/a&b/map.json?x=1&y=2#base",
value: { imports: { alias: "./alias.js" } },
},
specifiers: ["alias"],
});
assert.equal(
escapedBase.ok,
true,
escapedBase.ok ? "" : escapedBase.error.message,
);
assert.deepEqual(escapedBase.value.resolutions[0], {
state: "resolved",
url: "https://maps.invalid/maps/a&b/alias.js",
});
assert.deepEqual(
site.requests,
before,
"Synthetic-context tracing refetched or executed a website asset",
);
return {
independent_http_importer_and_https_map_base: true,
cancellation_cleanup: true,
native_warnings_retained: true,
cli: true,
stdio_mcp: true,
public_trace_cases: 8,
literal_imports: 11,
computed_unknowns: 1,
native_loading_oracle: true,
independent_module_instances: 3,
import_map_scopes: true,
null_and_backtracking_errors: true,
query_fragment_identity: true,
source_identity_verified: true,
map_identity_verified: true,
implicit_refetches: 0,
};
} finally {
await client.close();
await transport.close();
await site.close();
await rm(root, { recursive: true, force: true });
}
}
const assertTrace = (evidence, oracle, site, scriptIndex) => {
const result = webModuleTraceResultSchema.parse(evidence.normalized_result);
assert.equal(result.source.script_index, scriptIndex);
assert.equal(result.parser.state, "parsed");
assert.equal(result.imports.length, 12);
assert.deepEqual(
result.imports.slice(0, 11).map((item) => item.resolution),
oracle.resolutions,
);
assert.deepEqual(result.imports[11].resolution, {
state: "unknown",
reason: "computed-specifier",
});
assert.ok(result.imports.every((item) => item.execution === "unknown"));
assert.equal(
result.imports[6].captured_candidates.length,
0,
"An unloaded lazy URL became a captured source",
);
for (const index of [2, 3]) {
assert.ok(result.imports[index].captured_candidates.length > 0);
assert.ok(
result.imports[index].captured_candidates.every(
(candidate) => candidate.match === "response-url-without-fragment",
),
);
}
assert.equal(result.imports[4].resolution.url, `${site.origin}/query.js?v=2`);
assert.equal(
result.importer.url,
`${site.origin}/scoped/main.js?build=2#entry`,
);
assert.equal(result.engine.id, "chromium-native-module-resolver");
return result;
};
const loadingOracle = async (executable, site) => {
const browser = await chromium.launch({
executablePath: executable,
headless: true,
timeout: 15000,
args: [
"--disable-dev-shm-usage",
...(process.env.REA_BROWSER_NO_SANDBOX === "true"
? ["--no-sandbox"]
: []),
],
});
try {
const page = await browser.newPage();
await page.goto(site.origin);
await page.waitForFunction(
() => globalThis.__moduleProof !== undefined,
undefined,
{ timeout: 10000 },
);
const proof = await page.evaluate(() => globalThis.__moduleProof);
assert.equal(proof.alias, "scoped");
assert.equal(proof.pkg, "pkg");
assert.equal(proof.same_module, true);
assert.equal(proof.different_fragments, true);
assert.equal(new Set(proof.instances).size, 3);
assert.deepEqual(proof.urls, [
`${site.origin}/query.js?v=1#one`,
`${site.origin}/query.js?v=1#two`,
`${site.origin}/query.js?v=2`,
]);
assert.ok(
!site.requests.some(
(url) =>
url.includes("lazy") ||
url.includes("escape") ||
url.includes("blocked"),
),
);
return proof;
} finally {
await browser.close();
}
};