fix(javascript): preserve and resolve complete source-map URLs (#1788)

Keep line-comment URL declarations intact and percent-decode local URL paths
once, preserving raw Evidence identity and artifact-root confinement.

Fixes #1781. Fixes #1782.

Assisted-by: Suika (gensokyo-skills:suika-scatter-gather-planning)
This commit is contained in:
Ysopwnable
2026-10-11 18:19:39 +08:00
committed by GitHub
parent a077e81bec
commit ce845ba428
4 changed files with 207 additions and 1 deletions
@@ -1106,6 +1106,33 @@ describe("contextual JavaScript module identity", () => {
});
describe("url-reference JavaScript artifact path resolution", () => {
it.each([
"app%00.js.map",
"app%FF.js.map",
"app%C0%AF.js.map",
"app%2ejs.map",
"nested%2fworker.js",
"nested%5cworker.js",
"../../outside.js.map",
])("rejects an inadmissible URL path: %s", (declaredPath) => {
const resolution = resolve({
declaredPath,
sourcePath: "dist/app.js",
context: "url-reference",
files: fileMap([
file("dist/app.js.map", "root"),
file("dist/nested/worker.js", "root"),
]),
});
expect(resolution).toMatchObject({
declared_path: declaredPath,
resolution_context: "url-reference",
resolution_status: "rejected",
resolved_path: null,
});
expect(resolution.limitations.length).toBeGreaterThan(0);
});
it("resolves sibling paths and rejects external URL forms", () => {
const files = fileMap([
file("dist/app.js", "root"),
@@ -162,6 +162,14 @@ const contextualCandidate = (
return unresolvedCandidate("external", [
"URL schemes and protocol-relative URLs are outside this local artifact path context.",
]);
if (context === "url-reference") {
const decoded = percentDecodeUrlPath(declared);
if (decoded === null)
return unresolvedCandidate("rejected", [
"The URL reference path percent-decodes to NUL or to bytes that are not UTF-8.",
]);
declared = decoded;
}
return relativeCandidate(input.sourcePath, declared);
};
@@ -353,7 +353,9 @@ export const addSourceMapDirectives = (
const urlOffset = commentSource.slice(equalsStart + 1).search(/\S/u);
if (urlOffset === -1) continue;
const declaredStart = equalsStart + 1 + urlOffset;
const terminator = commentSource.slice(declaredStart).search(/[\s*]/u);
const terminator = commentSource
.slice(declaredStart)
.search(commentSource.startsWith("//") ? /\s/u : /[\s*]/u);
const declaredEnd =
terminator === -1 ? commentSource.length : declaredStart + terminator;
if (declaredEnd === declaredStart) continue;
@@ -1,4 +1,5 @@
import { execFile } from "node:child_process";
import { createHash } from "node:crypto";
import { copyFile, mkdir, writeFile } from "node:fs/promises";
import { createRequire } from "node:module";
import { join, relative, resolve } from "node:path";
@@ -445,6 +446,174 @@ describe("Electron unavailable identity through stdio MCP", () => {
);
});
it("preserves complete line-comment source-map URLs through CLI and MCP", async () => {
const root = await createTestTempDirectory("rea-source-map-url-cli-");
const directives = [
"//# sourceMappingURL=app.js.map?variant=a*modern",
"//@ sourceMappingURL=app.js.map?variant=a*legacy",
"//# sourceMappingURL=app.js.map#first*section",
];
await writeFile(
join(root, "app.js"),
["export const value = 1;", ...directives, ""].join("\n"),
);
await writeFile(
join(root, "app.js.map"),
JSON.stringify({ version: 3, sources: [], names: [], mappings: "" }),
);
const { graph } = z
.object({ normalized_result: javascriptApplicationAnalysisResultSchema })
.parse(
await runCli(["analyze-javascript-application", root, "--json"]),
).normalized_result;
const map = graph.nodes.find(
({ kind, observations }) =>
kind === "source-map" &&
observations.some(({ properties }) => properties.path === "app.js.map"),
);
const references = map?.observations.filter(
({ label }) => label === "source map reference",
);
expect(references).toHaveLength(directives.length);
for (const [index, directive] of directives.entries()) {
const declaredUrl = directive.slice(directive.indexOf("=") + 1);
const digest = createHash("sha256").update(declaredUrl).digest("hex");
expect(references).toContainEqual(
expect.objectContaining({
properties: {
declared_url: declaredUrl,
declared_url_sha256: digest,
resolved_path: "app.js.map",
available: true,
},
evidence: expect.objectContaining({
location: {
available: true,
value: {
kind: "source-range",
source: "app.js",
start: { line: index + 2, column: 0 },
end: { line: index + 2, column: directive.length },
},
},
}),
}),
);
expect(graph.edges).toContainEqual(
expect.objectContaining({
relation: "maps_to",
target_node_id: map?.node_id,
properties: {
declared_url_sha256: digest,
resolved_path: "app.js.map",
},
}),
);
}
expect((await analyzeThroughStdioMcp(root)).graph).toEqual(graph);
}, 20_000);
it("resolves source-map URL paths once without changing their declarations", async () => {
const root = await createTestTempDirectory("rea-source-map-encoding-cli-");
const cases = [
["space%20part.js.map", "space part.js.map"],
["literal%25.js.map", "literal%.js.map"],
["encoded%2520.js.map", "encoded%20.js.map"],
["space%20part.js.map?v=1#section", "space part.js.map"],
["caf%C3%A9.js.map", "café.js.map"],
["literal%ZZ.js.map", "literal%ZZ.js.map"],
] as const;
for (const path of new Set(cases.map(([, path]) => path))) {
await writeFile(
join(root, path),
JSON.stringify({
version: 3,
file: path,
sources: [],
names: [],
mappings: "",
}),
);
}
await writeFile(
join(root, "app.js"),
cases.map(([url]) => `//# sourceMappingURL=${url}\n`).join("") +
'new Worker("./worker%20file.js");\n' +
'navigator.serviceWorker.register("./service%20worker.js");\n',
);
await writeFile(join(root, "worker file.js"), "export const worker = 1;\n");
await writeFile(
join(root, "service worker.js"),
"export const service = 2;\n",
);
const { graph } = z
.object({ normalized_result: javascriptApplicationAnalysisResultSchema })
.parse(
await runCli(["analyze-javascript-application", root, "--json"]),
).normalized_result;
for (const [index, [declaredUrl, path]] of cases.entries()) {
const map = graph.nodes.find(
({ kind, observations }) =>
kind === "source-map" &&
observations.some(({ properties }) => properties.path === path),
);
const digest = createHash("sha256").update(declaredUrl).digest("hex");
expect(map?.observations).toContainEqual(
expect.objectContaining({
properties: {
declared_url: declaredUrl,
declared_url_sha256: digest,
resolved_path: path,
available: true,
},
evidence: expect.objectContaining({
location: {
available: true,
value: {
kind: "source-range",
source: "app.js",
start: { line: index + 1, column: 0 },
end: {
line: index + 1,
column: "//# sourceMappingURL=".length + declaredUrl.length,
},
},
},
}),
}),
);
expect(graph.edges).toContainEqual(
expect.objectContaining({
relation: "maps_to",
target_node_id: map?.node_id,
properties: { declared_url_sha256: digest, resolved_path: path },
}),
);
}
for (const [kind, specifier, path] of [
["worker", "./worker%20file.js", "worker file.js"],
["service-worker", "./service%20worker.js", "service worker.js"],
]) {
const worker = graph.nodes.find((node) => node.kind === kind);
expect(worker?.observations).toContainEqual(
expect.objectContaining({
properties: expect.objectContaining({
declared_specifier: specifier,
resolved_path: path,
}),
}),
);
expect(graph.edges).toContainEqual(
expect.objectContaining({
source_node_id: worker?.node_id,
relation: "maps_to",
properties: { resolved_path: path },
}),
);
}
expect((await analyzeThroughStdioMcp(root)).graph).toEqual(graph);
}, 20_000);
describe("JavaScript application path CLI", () => {
it("accepts a relative local application path and preserves canonical Evidence identity", async () => {
const root = await createTestTempDirectory("rea-relative-application-cli-");