Files
rea/tests/boundary/cli/runtimeUrlPrefixMappingCli.test.ts
T
Kaoru0822 1639189f26 fix(javascript): keep encoded URL slashes inside one runtime path segment (#6259)
A url-prefix runtime mapping decoded the whole pathname suffix at once, so
`child%2Fpart.js` became the nested artifact path `child/part.js` and produced
a medium-confidence location match and an inferred observed_as edge.

Decode each URL path segment separately at the mapping boundary. A segment
that decodes to contain a slash, like a malformed or non-UTF-8 escape, has no
artifact path, so the runtime script stays unknown with
no-authorized-location-mapping. Encoded spaces, Unicode, %25 and the literal
%5C backslash name keep matching.

File-root and URL-prefix mappings now share one tri-state translation
(outside, untranslatable, translated) for matching and load-state scope.
Static nodes beneath a prefix whose runtime script or worker could not be
translated stay unknown instead of becoming not-observed-in-capture.

Fixes #5878

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 00:26:12 -07:00

280 lines
8.4 KiB
TypeScript

import { execFile } from "node:child_process";
import { mkdir, rm, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { promisify } from "node:util";
import { afterEach, expect, it } from "vitest";
import { z } from "zod";
import { createBrowserEvidence } from "../../../src/application/BrowserEvidence.js";
import { inspectWebPageInputSchema } from "../../../src/domain/browserObservation.js";
import { analyzeFixture } from "../../fixtures/javascriptRuntimeReconciliation.js";
import { passiveNetworkCapture } from "../../fixtures/passiveNetworkCapture.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
const execute = promisify(execFile);
const INTEGRATION_TEST_TIMEOUT_MS = 30_000;
const temporary: string[] = [];
afterEach(async () => {
await Promise.all(
temporary
.splice(0)
.map(async (path) => rm(path, { recursive: true, force: true })),
);
});
const reconciledSchema = z.object({
normalized_result: z.object({
reconciliations: z.array(
z.object({
entity_kind: z.string(),
runtime_node_id: z.string(),
static_node_id: z.string().nullable(),
status: z.string(),
basis: z.string(),
confidence: z.string(),
reason: z.string(),
}),
),
static_load_states: z.array(
z.object({
static_node_id: z.string(),
kind: z.string(),
status: z.string(),
reason: z.string(),
}),
),
graph: z.object({
nodes: z.array(
z.object({
node_id: z.string(),
observations: z.array(
z.object({
properties: z.record(z.string(), z.unknown()),
}),
),
}),
),
edges: z.array(
z.object({
relation: z.string(),
source_node_id: z.string(),
target_node_id: z.string(),
}),
),
}),
}),
});
/** Reconcile one source-free browser capture under `${origin}/assets/`. */
const reconcileThroughCli = async (
root: string,
analysis: Awaited<ReturnType<typeof analyzeFixture>>,
suffixes: readonly string[],
) => {
const inspection = passiveNetworkCapture([]);
const origin = inspection.target.origin;
inspection.frames = [
{ frame_id: "frame-main", parent_frame_id: null, url: origin, origin },
];
inspection.scripts = {
total: suffixes.length,
items: suffixes.map((suffix, index) => ({
script_key: `scr_${String(index).padStart(64, "0")}`,
frame_id: "frame-main",
url: `${origin}/assets/${suffix}`,
origin,
cdp_hash: null,
length: null,
is_module: true,
language: "JavaScript",
source_map_url: null,
resource_reconciliation: {
status: "unmatched",
reason: "no_exact_sanitized_url",
},
source: { included: false, reason: "not-requested" },
})),
};
const runtime = createBrowserEvidence(
"inspect_web_page",
inspectWebPageInputSchema.parse({
cdp_endpoint: "http://127.0.0.1:9222",
target_id: "page",
allowed_origins: [origin],
observation_ms: 0,
include_script_sources: false,
}),
inspection,
{
id: "rea-cdp-browser",
name: "REA Chrome DevTools Protocol observation provider",
version: "2",
},
);
const inputPath = join(root, "reconciliation.json");
await writeFile(
inputPath,
JSON.stringify({
static_layers: [
{
role: "application",
analysis,
runtime_mappings: [
{
kind: "url-prefix",
prefix: `${origin}/assets/`,
artifact_prefix: "",
},
],
},
],
runtime_observations: [runtime],
}),
);
const { stdout, stderr } = await execute(
process.execPath,
["scripts/rea.mjs", "reconcile-javascript-runtime", inputPath, "--json"],
{ cwd: process.cwd(), maxBuffer: 16 * 1_024 * 1_024 },
);
expect(stderr).toBe("");
const result = reconciledSchema.parse(JSON.parse(stdout)).normalized_result;
const nodes = new Map(result.graph.nodes.map((node) => [node.node_id, node]));
const property = (nodeId: string | null, key: string) =>
nodeId === null
? null
: (nodes
.get(nodeId)
?.observations.map(({ properties }) => properties[key])
.find((value) => typeof value === "string") ?? null);
const pathOf = (nodeId: string | null) => property(nodeId, "path");
const scripts = new Map(
result.reconciliations
.filter(({ entity_kind }) => entity_kind === "script")
.map((item) => [
String(property(item.runtime_node_id, "location")).slice(
`${origin}/assets/`.length,
),
{
status: item.status,
basis: item.basis,
reason: item.reason,
path: pathOf(item.static_node_id),
observed_as: result.graph.edges.filter(
({ relation, source_node_id, target_node_id }) =>
relation === "observed_as" &&
[source_node_id, target_node_id].includes(item.runtime_node_id),
).length,
},
]),
);
const loadStates = new Map(
result.static_load_states
.filter(({ kind }) => kind === "javascript-asset")
.map((state) => [
pathOf(state.static_node_id),
{ status: state.status, reason: state.reason },
]),
);
return { scripts, loadStates };
};
const unmappedLocation = {
status: "unknown",
basis: "none",
reason: "no-authorized-location-mapping",
path: null,
observed_as: 0,
};
const mappedLocation = (path: string) => ({
status: "matched",
basis: "operator-url-mapping",
reason: "unique-location-match",
path,
observed_as: 1,
});
it(
"keeps an encoded slash inside one URL segment rather than naming a nested artifact",
async () => {
const root = await createTestTempDirectory("rea-url-prefix-segments-");
temporary.push(root);
await mkdir(join(root, "child"));
await Promise.all([
writeFile(join(root, "child", "part.js"), "export const nested = 1;\n"),
writeFile(join(root, "child%2Fpart.js"), "export const literal = 2;\n"),
writeFile(join(root, "sp ace.js"), "export const space = 3;\n"),
writeFile(join(root, "é.js"), "export const accent = 4;\n"),
]);
const analysis = await analyzeFixture(root);
const { scripts } = await reconcileThroughCli(root, analysis, [
"child/part.js",
"child/part.js?v=%2F#f%2F",
"child%2Fpart.js",
"child%2fpart.js",
"child%252Fpart.js",
"sp%20ace.js",
"%C3%A9.js",
"child/%E0.js",
]);
expect(Object.fromEntries(scripts)).toEqual({
"child/part.js": mappedLocation("child/part.js"),
"child/part.js?v=%2F#f%2F": mappedLocation("child/part.js"),
"child%2Fpart.js": unmappedLocation,
"child%2fpart.js": unmappedLocation,
"child%252Fpart.js": mappedLocation("child%2Fpart.js"),
"sp%20ace.js": mappedLocation("sp ace.js"),
"%C3%A9.js": mappedLocation("é.js"),
"child/%E0.js": unmappedLocation,
});
},
INTEGRATION_TEST_TIMEOUT_MS,
);
it(
"keeps static load state unknown beneath a prefix whose runtime script had no artifact path",
async () => {
const root = await createTestTempDirectory("rea-url-prefix-load-state-");
temporary.push(root);
await mkdir(join(root, "child"));
await Promise.all([
writeFile(join(root, "child", "part.js"), "export const nested = 1;\n"),
writeFile(join(root, "other.js"), "export const other = 2;\n"),
]);
const analysis = await analyzeFixture(root);
const control = await reconcileThroughCli(root, analysis, ["other.js"]);
expect(Object.fromEntries(control.loadStates)).toEqual({
"child/part.js": {
status: "not-observed-in-capture",
reason: "not-observed-in-bounded-capture",
},
"other.js": {
status: "loaded",
reason: "runtime-script-correspondence",
},
});
const encoded = await reconcileThroughCli(root, analysis, [
"other.js",
"child%2Fpart.js",
]);
expect(encoded.scripts.get("child%2Fpart.js")).toEqual(unmappedLocation);
expect(Object.fromEntries(encoded.loadStates)).toEqual({
"child/part.js": {
status: "unknown",
reason: "static-or-runtime-coverage-incomplete",
},
"other.js": {
status: "loaded",
reason: "runtime-script-correspondence",
},
});
},
INTEGRATION_TEST_TIMEOUT_MS,
);