From 5b28a8b331259e026678e681210e9c3c4f48f2fa Mon Sep 17 00:00:00 2001 From: Rudy Mizrahi Celekli <47457359+rudycelekli@users.noreply.github.com> Date: Mon, 5 Oct 2026 03:01:12 -0400 Subject: [PATCH] fix(workflows): preserve punctuation in filesystem source paths (#521) * fix(workflows): preserve punctuation in filesystem source paths Signed-off-by: RudyCelekli <47457359+rudycelekli@users.noreply.github.com> * fix(workflows): share scheme URL pattern in path normalization --------- Signed-off-by: RudyCelekli <47457359+rudycelekli@users.noreply.github.com> Co-authored-by: morluto <76467478+morluto@users.noreply.github.com> --- src/domain/sourceToBundleComparison.test.ts | 80 +++++++++++++++++++ src/domain/sourceToBundleSignals.ts | 11 ++- .../filesystem/sourceToBundlePaths.test.ts | 60 ++++++++++++++ 3 files changed, 149 insertions(+), 2 deletions(-) create mode 100644 tests/boundary/filesystem/sourceToBundlePaths.test.ts diff --git a/src/domain/sourceToBundleComparison.test.ts b/src/domain/sourceToBundleComparison.test.ts index bfa1a6b0a..ee5a59fd7 100644 --- a/src/domain/sourceToBundleComparison.test.ts +++ b/src/domain/sourceToBundleComparison.test.ts @@ -136,6 +136,86 @@ describe("historical source to bundle comparison", () => { }); }); +describe("source to bundle path syntax", () => { + it.each(["src/modified#part.ts", "src/modified?part.ts"])( + "preserves literal filesystem punctuation in %s", + (path) => { + const node = moduleNode("punctuated", { path }); + const graph = createJavaScriptApplicationGraph({ + schema: "JavaScriptApplicationGraph", + root_node_ids: [node.node_id], + nodes: [node], + edges: [], + coverage: { + status: "complete", + truncated: false, + omitted_count: 0, + limits: [], + }, + limitations: [], + }); + const result = compareSourceToBundle({ + reference: historicalGraph("complete", [path]), + application: { + evidenceId: EVIDENCE_ID, + rootArtifactSha256: HASH.artifact, + graph, + }, + }); + expect(item(result, path)).toMatchObject({ + status: "unknown", + current_node_ids: [node.node_id], + candidates: [ + expect.objectContaining({ + signals: expect.arrayContaining([ + expect.objectContaining({ kind: "current-path-exact" }), + ]), + }), + ], + }); + }, + ); + + it.each([ + "https://example.test/src/modified.ts?revision=2#section", + "webpack://fixture/./src/modified.ts?revision=2#section", + ])("still strips URL query and fragment from %s", (path) => { + const node = moduleNode("url", { path }); + const graph = createJavaScriptApplicationGraph({ + schema: "JavaScriptApplicationGraph", + root_node_ids: [node.node_id], + nodes: [node], + edges: [], + coverage: { + status: "complete", + truncated: false, + omitted_count: 0, + limits: [], + }, + limitations: [], + }); + const result = compareSourceToBundle({ + reference: historicalGraph("complete", ["src/modified.ts"]), + application: { + evidenceId: EVIDENCE_ID, + rootArtifactSha256: HASH.artifact, + graph, + }, + }); + expect(item(result, "src/modified.ts")).toMatchObject({ + status: "unknown", + current_node_ids: [node.node_id], + candidates: [ + expect.objectContaining({ + signals: expect.arrayContaining([ + expect.objectContaining({ kind: "current-path-suffix" }), + ]), + }), + ], + }); + }); +}); + describe("source to bundle complete metadata", () => { it("accepts long source metadata and all reported limitations", () => { const result = compareSourceToBundle({ diff --git a/src/domain/sourceToBundleSignals.ts b/src/domain/sourceToBundleSignals.ts index 7df07b90a..a182acf52 100644 --- a/src/domain/sourceToBundleSignals.ts +++ b/src/domain/sourceToBundleSignals.ts @@ -268,9 +268,16 @@ const addPath = ( if (value !== null) paths.push({ kind, value }); }; +/** Matches a `scheme://...` URL prefix; bare filesystem paths keep `?`/`#`. */ +const SCHEME_URL = /^[a-z][a-z0-9+.-]*:\/\//iu; +/** Matches a `scheme://...` URL prefix with any run of slashes, for stripping. */ +const SCHEME_URL_PREFIX = /^[a-z][a-z0-9+.-]*:\/\/+/iu; + const normalizeCurrentPath = (raw: string): string | null => { - const withoutQuery = raw.split(/[?#]/u, 1)[0] ?? ""; - const withoutScheme = withoutQuery.replace(/^[a-z][a-z0-9+.-]*:\/\/+/iu, ""); + const withoutQuery = SCHEME_URL.test(raw) + ? (raw.split(/[?#]/u, 1)[0] ?? "") + : raw; + const withoutScheme = withoutQuery.replace(SCHEME_URL_PREFIX, ""); const parts = withoutScheme .replaceAll("\\", "/") .split("/") diff --git a/tests/boundary/filesystem/sourceToBundlePaths.test.ts b/tests/boundary/filesystem/sourceToBundlePaths.test.ts new file mode 100644 index 000000000..888d95a5d --- /dev/null +++ b/tests/boundary/filesystem/sourceToBundlePaths.test.ts @@ -0,0 +1,60 @@ +import { mkdir, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { expect, it } from "vitest"; + +import { analyzeJavaScriptApplication } from "../../../src/application/JavaScriptApplicationService.js"; +import { parseApplicationGraphEvidence } from "../../../src/application/JavaScriptApplicationEvidenceGraph.js"; +import { importReferenceSource } from "../../../src/application/ReferenceSourceImport.js"; +import { compareSourceToBundle } from "../../../src/domain/sourceToBundleComparison.js"; +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; + +it("keeps literal hash characters in historical/current filesystem paths", async () => { + const root = await createTestTempDirectory("rea-source-path-"); + const previous = join(root, "previous"); + const current = join(root, "current"); + await Promise.all([mkdir(previous), mkdir(current)]); + await Promise.all([ + writeFile(join(previous, "worker#main.js"), "export const value = 1;"), + writeFile(join(current, "worker#main.js"), "export const value = 2;"), + ]); + const reference = await importReferenceSource({ + root: previous, + caller: "source-to-bundle-path-test", + policy: { secretPatterns: [] }, + }); + if (!reference.ok) throw new Error(reference.error.message); + const analyzed = await analyzeJavaScriptApplication({ input_path: current }); + if (!analyzed.ok) throw analyzed.error; + const application = parseApplicationGraphEvidence(analyzed.value); + const comparison = compareSourceToBundle({ + reference: reference.value, + application: { + evidenceId: application.evidence.evidence_id, + rootArtifactSha256: application.rootArtifactSha256, + graph: application.graph, + }, + }); + + expect(reference.value.entries).toContainEqual( + expect.objectContaining({ + kind: "file", + path: "worker#main.js", + content_state: "hashed", + }), + ); + expect(comparison.items).toContainEqual( + expect.objectContaining({ + source_path: "worker#main.js", + candidates: expect.arrayContaining([ + expect.objectContaining({ + signals: expect.arrayContaining([ + expect.objectContaining({ + kind: "current-path-exact", + source_value: "worker#main.js", + }), + ]), + }), + ]), + }), + ); +});