import { Readable } from "node:stream"; import { mkdir, rm, symlink, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { createPackageWithOptions } from "@electron/asar"; import { describe, expect, it } from "vitest"; import { createTestTempDirectory } from "./fixtures/temporaryDirectory.js"; import { readJavaScriptArtifactFiles } from "../src/application/JavaScriptArtifactFiles.js"; import { reconstructJavaScriptArtifact } from "../src/application/JavaScriptArtifactReconstruction.js"; import { artifactLimitsForReconstruction, javascriptArtifactReconstructionInputSchema, } from "../src/application/JavaScriptArtifactReconstructionInput.js"; import { scanArtifactInventory } from "../src/application/ArtifactInventory.js"; import { type ArtifactEntry, type ArtifactReader, } from "../src/artifacts/ArtifactReader.js"; import { parseJavaScriptApplicationGraph } from "../src/domain/javascriptApplicationGraph.js"; import { javaScriptApplicationAnalysisResultV2Schema } from "../src/domain/javascriptApplicationAnalysis.js"; import { createJavaScriptSemanticGraph } from "../src/domain/javascriptSemanticGraph.js"; import { parseJavaScriptSemanticGraph } from "../src/domain/javascriptSemanticGraphSerialization.js"; import { writeJavaScriptArtifactFixture } from "./fixtures/javascriptArtifactApplication.js"; describe("JavaScript artifact reconstruction", () => { it("reconstructs package, Electron roles, Webpack/Rspack modules, and cross-layer facts without execution", async () => { const root = await fixtureDirectory(); Reflect.deleteProperty(globalThis, "__rea_bundle_executed"); const result = await reconstructJavaScriptArtifact({ input_path: root, source_map_read_approved: true, }); const graph = parseJavaScriptApplicationGraph(result.graph); const semanticGraph = parseJavaScriptSemanticGraph(result.semantic_graph); expect(Reflect.get(globalThis, "__rea_bundle_executed")).toBeUndefined(); expect(result.input_path).toBe(root); expect(result.statistics).toMatchObject({ modules: 4, parse_failures: 0, omitted_text_files: 0, policy_filtered_text_files: 0, }); expect(graph.nodes.map(({ kind }) => kind)).toEqual( expect.arrayContaining([ "package", "artifact", "electron-main", "electron-preload", "electron-renderer", "javascript-asset", "javascript-chunk", "javascript-module", "worker", "service-worker", "endpoint", "storage", "source-map", "source-module", "native-addon", ]), ); expect( graph.nodes.filter(({ kind }) => kind === "javascript-chunk"), ).toEqual( expect.arrayContaining([ expect.objectContaining({ observations: expect.arrayContaining([ expect.objectContaining({ properties: expect.objectContaining({ bundler: "webpack" }), }), ]), }), expect.objectContaining({ observations: expect.arrayContaining([ expect.objectContaining({ properties: expect.objectContaining({ bundler: "rspack" }), }), ]), }), ]), ); expect( graph.nodes.filter(({ kind }) => kind === "javascript-module"), ).toEqual( expect.arrayContaining([ expect.objectContaining({ observations: expect.arrayContaining([ expect.objectContaining({ properties: expect.objectContaining({ module_key: "1", structural_fingerprint_algorithm: "babel-ast-v1", }), }), ]), }), ]), ); expect(graph.edges.map(({ relation }) => relation)).toEqual( expect.arrayContaining([ "contains", "loads", "imports", "maps_to", "exposes", "calls", "persists_to", ]), ); expect(semanticGraph.application_graph_id).toBe(graph.graph_id); const { graph_id: _semanticGraphId, ...semanticGraphInput } = semanticGraph; const mismatchedSemanticGraph = createJavaScriptSemanticGraph({ ...semanticGraphInput, application_graph_id: `jag_${"f".repeat(64)}`, }); const { electron_summary: summary, ...analysisResult } = result; expect(() => javaScriptApplicationAnalysisResultV2Schema.parse({ ...analysisResult, schema_version: 2, summary, semantic_graph: mismatchedSemanticGraph, }), ).toThrow(/must commit the containing application graph/u); expect( semanticGraph.nodes.some( ({ application_node_ids: identifiers }) => identifiers.length > 0, ), ).toBe(true); expect(semanticGraph.relations.length).toBeGreaterThan(0); const mainRole = graph.nodes.find( (node) => node.kind === "electron-main" && node.observations.some( ({ properties }) => properties.declared_path === "main.js", ), ); const preloadRole = graph.nodes.find( (node) => node.kind === "electron-preload" && node.observations.some( ({ properties }) => properties.declared_path === "preload.js", ), ); expect(mainRole?.observations[0]?.properties).toMatchObject({ declared_path: "main.js", resolution_context: "package-entrypoint", resolved_path: "main.js", resolution_status: "resolved", limitations: [], }); expect(preloadRole?.observations[0]?.properties).toMatchObject({ declared_path: "preload.js", resolution_context: "filesystem-expression", resolved_path: "preload.js", resolution_status: "resolved", limitations: [], }); const mainAsset = graph.nodes.find( (node) => node.kind === "javascript-asset" && node.observations.some( ({ evidence }) => evidence.location.available && evidence.location.value.kind === "artifact-path" && evidence.location.value.path === "main.js", ), ); if (mainAsset === undefined) throw new Error("Expected main JavaScript asset"); const mainSemanticNodes = semanticGraph.nodes.filter( ({ identity }) => identity.module_path === "main.js", ); const linkedMainSemanticNodes = mainSemanticNodes.filter( ({ application_node_ids: identifiers }) => identifiers.includes(mainAsset.node_id), ); expect(linkedMainSemanticNodes.length).toBeGreaterThan(0); expect(linkedMainSemanticNodes.length).toBeLessThan( mainSemanticNodes.length, ); for (const role of [mainRole, preloadRole]) { expect(role).toBeDefined(); expect( graph.edges.some( (edge) => edge.source_node_id === role?.node_id && edge.relation === "maps_to", ), ).toBe(true); } const endpointJson = JSON.stringify( graph.nodes.filter(({ kind }) => kind === "endpoint"), ); expect(endpointJson).toContain("token=%5BREDACTED%5D"); expect(endpointJson).not.toContain("fixture-secret"); }); it("produces deterministic ASAR graphs with unpacked native linkage and complete paths/digests", async () => { const root = await createTestTempDirectory("rea-javascript-asar-"); const source = join(root, "source"); await mkdir(source); await writeJavaScriptArtifactFixture(source); const archive = join(root, "app.asar"); await createPackageWithOptions(source, archive, { unpack: "**/*.node" }); const first = await reconstructJavaScriptArtifact({ input_path: archive, format: "asar", source_map_read_approved: true, }); const second = await reconstructJavaScriptArtifact({ input_path: archive, format: "asar", source_map_read_approved: true, }); expect(first.graph).toEqual(second.graph); expect(first.inventory_graph_sha256).toBe(second.inventory_graph_sha256); expect(first.graph.graph_id).toBe(second.graph.graph_id); expect(first.graph.nodes).toEqual( expect.arrayContaining([ expect.objectContaining({ kind: "asar-entry", observations: expect.arrayContaining([ expect.objectContaining({ properties: expect.objectContaining({ entry_sha256: expect.stringMatching(/^[a-f0-9]{64}$/u), inventory_artifact_id: expect.stringMatching(/^art_[a-f0-9]{64}$/u), }), }), ]), }), expect.objectContaining({ kind: "native-addon", observations: expect.arrayContaining([ expect.objectContaining({ properties: expect.objectContaining({ path: "native/addon.node", unpacked: true, }), }), ]), }), ]), ); expect(first.graph.edges).toContainEqual( expect.objectContaining({ relation: "loads", properties: expect.objectContaining({ resolved_path: "native/addon.node", }), }), ); }); it("keeps ASAR analysis usable when unpacked native companion bytes are absent", async () => { const root = await createTestTempDirectory("rea-javascript-asar-missing-"); const source = join(root, "source"); await mkdir(source); await writeJavaScriptArtifactFixture(source); const archive = join(root, "app.asar"); await createPackageWithOptions(source, archive, { unpack: "**/*.node" }); await rm(join(`${archive}.unpacked`, "native", "addon.node")); const snapshot = await scanArtifactInventory(archive, { maxEntries: 8_000, maxTotalBytes: 512 * 1024 * 1024, maxEntryBytes: 128 * 1024 * 1024, maxCompressionRatio: 1_000, maxDepth: 64, maxPathBytes: 4_096, }); const missingNative = snapshot.occurrences.find( ({ logical_path }) => logical_path === "native/addon.node", ); expect(missingNative).toMatchObject({ artifact_id: null, hash_status: "unavailable", limitations: expect.arrayContaining([ "ASAR unpacked companion bytes were unavailable; no content hash or child artifact was produced.", ]), }); const result = await reconstructJavaScriptArtifact({ input_path: archive, format: "asar", source_map_read_approved: true, }); expect(result.statistics.parsed_javascript_files).toBeGreaterThan(0); expect(result.graph.nodes).toEqual( expect.arrayContaining([ expect.objectContaining({ kind: "javascript-asset" }), ]), ); }); it("recurses into filesystem-backed ASAR containers without losing container-relative paths", async () => { const root = await createTestTempDirectory("rea-javascript-nested-asar-"); const source = await fixtureDirectory(); const outer = join(root, "outer"); const resources = join(outer, "resources"); await mkdir(resources, { recursive: true }); await createPackageWithOptions(source, join(resources, "app.asar"), { unpack: "**/*.node", }); const result = await reconstructJavaScriptArtifact({ input_path: outer, source_map_read_approved: true, }); expect(result.statistics.nested_asar_containers).toBe(1); expect(result.statistics.modules).toBe(4); expect(result.graph.nodes).toEqual( expect.arrayContaining([ expect.objectContaining({ kind: "artifact", observations: expect.arrayContaining([ expect.objectContaining({ label: "resources/app.asar", properties: expect.objectContaining({ format: "asar" }), }), ]), }), expect.objectContaining({ kind: "javascript-asset", observations: expect.arrayContaining([ expect.objectContaining({ properties: expect.objectContaining({ path: "resources/app.asar/main.js", }), }), ]), }), ]), ); }); it("discovers source maps without reading them until separately approved", async () => { const root = await fixtureDirectory(); const unapproved = await reconstructJavaScriptArtifact({ input_path: root, source_map_read_approved: false, }); const approved = await reconstructJavaScriptArtifact({ input_path: root, source_map_read_approved: true, }); expect( unapproved.graph.nodes.some(({ kind }) => kind === "source-map"), ).toBe(true); expect( unapproved.graph.nodes.some(({ kind }) => kind === "source-module"), ).toBe(false); expect(unapproved.graph.coverage).toMatchObject({ status: "partial", truncated: false, omitted_count: null, }); expect(unapproved.statistics.policy_filtered_text_files).toBe(1); expect(unapproved.graph.nodes).toEqual( expect.arrayContaining([ expect.objectContaining({ kind: "unknown", observations: expect.arrayContaining([ expect.objectContaining({ properties: expect.objectContaining({ operation: "parse-local-source-map", }), evidence: expect.objectContaining({ state: "unavailable" }), }), ]), }), ]), ); expect( approved.graph.nodes.some(({ kind }) => kind === "source-module"), ).toBe(true); }); it("reports text and AST limits as truncation instead of absence", async () => { const root = await fixtureDirectory(); const result = await reconstructJavaScriptArtifact({ input_path: root, source_map_read_approved: true, limits: { max_text_file_bytes: 128, max_total_text_bytes: 1_024, max_ast_nodes: 50, }, }); expect(result.statistics.limit_omitted_text_files).toBeGreaterThan(0); expect(result.graph.coverage).toMatchObject({ status: "partial", truncated: true, }); expect(result.graph.coverage.omitted_count).toBeGreaterThan(0); expect(result.graph.nodes.some(({ kind }) => kind === "unknown")).toBe( true, ); expect(result.graph.limitations.join(" ")).toMatch(/incomplete|bound/iu); }); it("uses an unknown omission count when AST traversal reaches its bound", async () => { const root = await createTestTempDirectory("rea-javascript-ast-limit-"); await writeFile( join(root, "large.js"), [ 'import "./unobserved.js";', ...Array.from( { length: 100 }, (_, index) => `export const value${String(index)} = ${String(index)};`, ), ].join("\n"), ); const result = await reconstructJavaScriptArtifact({ input_path: root, limits: { max_ast_nodes: 50 }, }); expect(result.statistics.limit_omitted_text_files).toBe(0); expect(result.statistics.truncated_scopes).toBe(1); expect(result.graph.coverage).toMatchObject({ status: "partial", truncated: true, omitted_count: null, }); const derivedImport = result.graph.edges.find( ({ relation, properties }) => relation === "imports" && properties.specifier === "./unobserved.js", ); expect(derivedImport?.evidence.coverage).toEqual({ status: "partial", truncated: true, omitted_count: null, limits: expect.arrayContaining([ { name: "max-ast-nodes", value: 50, unit: "items" }, ]), }); }); it("does not follow symlinks or read oversized JavaScript text", async () => { const root = await fixtureDirectory(); const outside = await createTestTempDirectory("rea-javascript-outside-"); const outsideFile = join(outside, "secret.js"); await writeFile( outsideFile, 'fetch("https://outside-secret.invalid/credential");', ); await symlink(outsideFile, join(root, "escape.js")); await writeFile( join(root, "oversized.js"), `const oversizedSecret = "${"private-marker-".repeat(256)}";`, ); const result = await reconstructJavaScriptArtifact({ input_path: root, limits: { max_text_file_bytes: 512 }, }); const encoded = JSON.stringify(result); expect(result.statistics.limit_omitted_text_files).toBeGreaterThan(0); expect(result.graph.coverage).toMatchObject({ status: "partial", truncated: true, }); expect(encoded).not.toContain(outsideFile); expect(encoded).not.toContain("outside-secret.invalid"); expect(encoded).not.toContain("private-marker-private-marker"); }); it("keeps malformed JavaScript, package metadata, and source maps as explicit unknowns", async () => { const root = await fixtureDirectory(); await Promise.all([ writeFile(join(root, "package.json"), "{"), writeFile(join(root, "broken.js"), "function broken( {"), writeFile(join(root, "renderer", "renderer.js.map"), "{"), ]); const result = await reconstructJavaScriptArtifact({ input_path: root, source_map_read_approved: true, }); const unknownOperations = result.graph.nodes .filter(({ kind }) => kind === "unknown") .flatMap(({ observations }) => observations.map(({ properties }) => properties.operation), ); expect(result.statistics.parse_failures).toBeGreaterThan(0); expect(result.graph.coverage).toMatchObject({ status: "partial", truncated: false, omitted_count: null, }); expect(unknownOperations).toEqual( expect.arrayContaining([ "parse-javascript", "parse-package-json", "parse-local-source-map", ]), ); }); it("applies one source-map source budget across every local map", async () => { const root = await createTestTempDirectory("rea-javascript-source-maps-"); await Promise.all([ writeFile( join(root, "a.js.map"), JSON.stringify({ version: 3, sources: ["a-one.ts", "a-two.ts"], sourcesContent: ["one", "two"], names: [], mappings: "", }), ), writeFile( join(root, "b.js.map"), JSON.stringify({ version: 3, sources: ["b-one.ts", "b-two.ts"], sourcesContent: ["three", "four"], names: [], mappings: "", }), ), ]); const result = await reconstructJavaScriptArtifact({ input_path: root, source_map_read_approved: true, limits: { max_source_map_sources: 3 }, }); expect( result.graph.nodes.filter(({ kind }) => kind === "source-module"), ).toHaveLength(3); expect(result.graph.coverage).toMatchObject({ status: "partial", truncated: true, omitted_count: null, }); }); it("bounds repeated content observations while retaining every containment path", async () => { const root = await createTestTempDirectory("rea-javascript-observations-"); await Promise.all( Array.from({ length: 70 }, (_, index) => writeFile( join(root, `duplicate-${String(index).padStart(2, "0")}.js`), "export const same = 1;\n", ), ), ); const result = await reconstructJavaScriptArtifact({ input_path: root }); const duplicateNode = result.graph.nodes.find( ({ kind, observations }) => kind === "javascript-asset" && observations.some(({ properties }) => String(properties.path).startsWith("duplicate-"), ), ); const duplicateEdges = result.graph.edges.filter( ({ relation, properties }) => relation === "contains" && String(properties.path).startsWith("duplicate-"), ); expect(duplicateNode?.observations).toHaveLength(64); expect(duplicateEdges).toHaveLength(70); expect(result.graph.coverage).toMatchObject({ status: "partial", truncated: true, omitted_count: 6, }); }, 15_000); it("rejects traversal from a production reader seam and malformed ASAR containers", async () => { const root = await fixtureDirectory(); const input = javascriptArtifactReconstructionInputSchema.parse({ input_path: root, }); const snapshot = await scanArtifactInventory( root, artifactLimitsForReconstruction(input), ); await expect( readJavaScriptArtifactFiles(new TraversalReader(), snapshot, input), ).rejects.toMatchObject({ reason: "path" }); const malformed = join(root, "malformed.asar"); await writeFile(malformed, "not an asar"); await expect( reconstructJavaScriptArtifact({ input_path: malformed }), ).rejects.toMatchObject({ name: "ArtifactReaderFailure", reason: "format", }); }); it("preserves cancellation and explicit format diagnostics", async () => { const root = await fixtureDirectory(); const controller = new AbortController(); controller.abort(); await expect( reconstructJavaScriptArtifact({ input_path: root }, controller.signal), ).rejects.toMatchObject({ reason: "cancelled" }); await expect( reconstructJavaScriptArtifact({ input_path: root, format: "asar" }), ).rejects.toMatchObject({ reason: "format", message: expect.stringContaining(root), }); expect(() => javascriptArtifactReconstructionInputSchema.parse({ input_path: root, limits: { max_entries: 100_000 }, }), ).toThrow(/graph contract/iu); }); }); class TraversalReader implements ArtifactReader { readonly format = "directory" as const; async *entries(): AsyncIterable { yield { path: "../escape.js", kind: "file", declaredSize: 1, compressedSize: null, executable: false, encrypted: false, byteOffset: null, declaredSha256: null, unpacked: false, limitations: [], adapterKey: "/tmp/escape.js", }; } open(): Promise { return Promise.resolve(Readable.from("x")); } close(): Promise { return Promise.resolve(); } provenance(): readonly [] { return []; } } const fixtureDirectory = async (): Promise => { const root = await createTestTempDirectory("rea-javascript-artifact-"); await writeJavaScriptArtifactFixture(root); return root; };