Preserve missing ASAR companion occurrence identity, declared size and unknown content in graph nodes and resolution limitations. Apply URL text semantics once for ESM lookup while preserving literal CommonJS paths and original specifiers. Keep CJS wrapper var bindings and assignment history, recognizing only untouched wrapper redeclarations. Validation: real ASAR producer workflow and CLI/MCP path/coverage cases; semantic reassignment controls. Context7 Node documentation and direct Node wrapper execution confirm wrapper behavior. Fixes #3213 Fixes #3214 Fixes #3223 Related: #3278, #3254 Co-authored-by: Geronimo <varungore1@gmail.com> Co-authored-by: Paolo Valletta <paovalletta@hotmail.it>
775 lines
27 KiB
TypeScript
775 lines
27 KiB
TypeScript
import {
|
|
primitiveCandidateExpansionSource,
|
|
primitiveByteExpansionSource,
|
|
} from "../../../fixtures/javascriptPrimitiveExpansion.js";
|
|
import { createHash } from "node:crypto";
|
|
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
|
|
import { createPackageWithOptions } from "@electron/asar";
|
|
import { expect, it } from "vitest";
|
|
|
|
import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js";
|
|
|
|
import {
|
|
reconstructJavaScriptArtifact,
|
|
readJavaScriptArtifactFiles,
|
|
} from "../../../support/javascriptApplicationScope.js";
|
|
import {
|
|
analyzeJavaScriptArtifactFiles,
|
|
analyzeAndProjectJavaScriptArtifactFiles,
|
|
} from "../../../../src/application/javascript/JavaScriptArtifactAnalysis.js";
|
|
import { buildJavaScriptArtifactGraph } from "../../../../src/application/javascript/JavaScriptArtifactGraphBuilder.js";
|
|
import {
|
|
buildJavaScriptSemanticGraph,
|
|
createJavaScriptSemanticGraphProjection,
|
|
projectJavaScriptSemanticFileSteps,
|
|
} from "../../../../src/application/javascript/JavaScriptSemanticGraphBuilder.js";
|
|
import {
|
|
projectJavaScriptModuleSemantics,
|
|
type JavaScriptModuleSemanticIr,
|
|
} from "../../../../src/domain/javascript/javascriptModuleSemanticIr.js";
|
|
import type { JavaScriptStaticAnalysis } from "../../../../src/domain/javascript/javascriptStaticAnalysisTypes.js";
|
|
import type { JavaScriptArtifactFile } from "../../../../src/domain/javascript/javascriptArtifactFiles.js";
|
|
import { completeJavaScriptAnalysisSteps } from "../../../../src/application/javascript/JavaScriptAnalysisControl.js";
|
|
import {
|
|
writeJavaScriptAnalysisTransfer,
|
|
readJavaScriptAnalysisTransfer,
|
|
} from "../../../../src/javascript/analysis/JavaScriptAnalysisTransfer.js";
|
|
import { javaScriptWorkerStaticAnalysisSchema } from "../../../../src/javascript/analysis/JavaScriptAnalysisWorkerSchemas.js";
|
|
import { createJavaScriptArtifactReader } from "../../../../src/artifacts/javascript/JavaScriptArtifactReader.js";
|
|
import {
|
|
scanCanonicalArtifactInventory,
|
|
scanArtifactInventory,
|
|
} from "../../../fixtures/artifactInventory.js";
|
|
import { parseJavaScriptApplicationGraph } from "../../../../src/domain/javascript/javascriptApplicationGraph.js";
|
|
import { javascriptApplicationAnalysisResultSchema } from "../../../../src/domain/javascript/javascriptApplicationAnalysis.js";
|
|
import {
|
|
createJavaScriptSemanticGraph,
|
|
isValidatedImmutableJavaScriptSemanticGraph,
|
|
} from "../../../../src/domain/javascript/javascriptSemanticGraph.js";
|
|
import { parseJavaScriptSemanticGraph } from "../../../../src/domain/javascript/javascriptSemanticGraphSerialization.js";
|
|
import { writeJavaScriptArtifactFixture } from "../../../fixtures/javascriptArtifactApplication.js";
|
|
|
|
it("reports file progress and honors cancellation before the next source", async () => {
|
|
const root = await fixtureDirectory();
|
|
const controller = new AbortController();
|
|
const progress: string[] = [];
|
|
await expect(
|
|
reconstructJavaScriptArtifact({ input_path: root }, controller.signal, {
|
|
report: async (event) => {
|
|
if (event.phase === "parse_javascript_source") {
|
|
progress.push(event.message);
|
|
if (progress.length === 2) controller.abort();
|
|
}
|
|
},
|
|
}),
|
|
).rejects.toMatchObject({
|
|
_tag: "AnalysisCancelledError",
|
|
partialObservation: { operation: "analyze_javascript_application" },
|
|
});
|
|
expect(progress).toHaveLength(2);
|
|
expect(progress[0]).not.toBe(progress[1]);
|
|
});
|
|
|
|
it("preserves graph commitments and export shapes when consuming file-local IR", async () => {
|
|
const root = await fixtureDirectory();
|
|
await writeFile(
|
|
join(root, "exports.mjs"),
|
|
`
|
|
import { readFile } from "node:fs";
|
|
export { readFile as read };
|
|
export function create(value) { return { value, nested: { enabled: true } }; }
|
|
export function configured(condition) { return { values: [1, null, undefined], choice: condition ? 1 : 2, ...external }; }
|
|
function unexported() { return "private"; }
|
|
`,
|
|
);
|
|
const snapshot = await scanCanonicalArtifactInventory(root, {});
|
|
const reader = createJavaScriptArtifactReader(root, "directory");
|
|
try {
|
|
const files = await readJavaScriptArtifactFiles(reader, snapshot);
|
|
const full = analyzeJavaScriptArtifactFiles(files);
|
|
const projection = createJavaScriptSemanticGraphProjection();
|
|
const compact = await analyzeAndProjectJavaScriptArtifactFiles(
|
|
files,
|
|
projection.projectFile,
|
|
);
|
|
const graph = buildJavaScriptArtifactGraph(snapshot, files, full);
|
|
expect(buildJavaScriptArtifactGraph(snapshot, files, compact)).toEqual(
|
|
graph,
|
|
);
|
|
const semantic = projection.finish(snapshot.manifest.root_sha256, graph);
|
|
expect(semantic).toEqual(
|
|
buildJavaScriptSemanticGraph({
|
|
rootArtifactSha256: snapshot.manifest.root_sha256,
|
|
applicationGraph: graph,
|
|
analysis: full,
|
|
}),
|
|
);
|
|
expect(
|
|
semantic.nodes.some(
|
|
({ application_node_ids }) => application_node_ids.length > 0,
|
|
),
|
|
).toBe(true);
|
|
const exported = compact.files.find(
|
|
({ file }) => file.path === "exports.mjs",
|
|
);
|
|
expect(exported?.semantic?.ir.callables.map(({ name }) => name)).toEqual([
|
|
"create",
|
|
"configured",
|
|
]);
|
|
expect(
|
|
full.files
|
|
.find(({ file }) => file.path === "exports.mjs")
|
|
?.semantic?.ir.callables.map(({ name }) => name),
|
|
).toContain("unexported");
|
|
expect(parseJavaScriptSemanticGraph(semantic)).toEqual(semantic);
|
|
const transferred = createJavaScriptSemanticGraphProjection();
|
|
const wireRoot = await createTestTempDirectory("rea-javascript-transfer-");
|
|
const decodedModules = new Map<string, JavaScriptModuleSemanticIr>();
|
|
const decodedStatic = new Map<string, JavaScriptStaticAnalysis>();
|
|
let rejectedInvalidTransfer = false;
|
|
for (const [index, analyzed] of full.files.entries()) {
|
|
if (analyzed.javascript !== null) {
|
|
const raw: unknown = JSON.parse(JSON.stringify(analyzed.javascript));
|
|
const decodedFacts = javaScriptWorkerStaticAnalysisSchema.parse(raw);
|
|
expect(decodedFacts).toEqual(analyzed.javascript);
|
|
decodedStatic.set(analyzed.file.path, decodedFacts);
|
|
}
|
|
if (analyzed.semantic === null) continue;
|
|
const fragment = await completeJavaScriptAnalysisSteps(
|
|
projectJavaScriptSemanticFileSteps(
|
|
analyzed.file,
|
|
analyzed.semantic.ir,
|
|
transferred.remainingFileNodeBudget(),
|
|
),
|
|
);
|
|
const module = projectJavaScriptModuleSemantics(analyzed.semantic.ir);
|
|
const wirePath = join(wireRoot, `${String(index)}.jsonl`);
|
|
const descriptor = await writeJavaScriptAnalysisTransfer(
|
|
wirePath,
|
|
module,
|
|
fragment,
|
|
);
|
|
const decoded = await readJavaScriptAnalysisTransfer(
|
|
wirePath,
|
|
descriptor,
|
|
analyzed.file,
|
|
);
|
|
expect(decoded.module).toEqual(module);
|
|
decodedModules.set(analyzed.file.path, decoded.module);
|
|
transferred.adoptFileProjection(decoded.projection);
|
|
if (!rejectedInvalidTransfer) {
|
|
await rejectInvalidTransfer(
|
|
wirePath,
|
|
descriptor,
|
|
analyzed.file,
|
|
wireRoot,
|
|
);
|
|
rejectedInvalidTransfer = true;
|
|
}
|
|
}
|
|
const decodedGraph = buildJavaScriptArtifactGraph(snapshot, files, {
|
|
...compact,
|
|
files: compact.files.map((analyzed) => {
|
|
const module = decodedModules.get(analyzed.file.path);
|
|
return {
|
|
...analyzed,
|
|
javascript:
|
|
decodedStatic.get(analyzed.file.path) ?? analyzed.javascript,
|
|
semantic: module === undefined ? analyzed.semantic : { ir: module },
|
|
};
|
|
}),
|
|
});
|
|
expect(decodedGraph).toEqual(graph);
|
|
const resealed = await completeJavaScriptAnalysisSteps(
|
|
transferred.finishImmutableSteps(
|
|
snapshot.manifest.root_sha256,
|
|
decodedGraph,
|
|
),
|
|
);
|
|
expect(resealed).toEqual(semantic);
|
|
expect(isValidatedImmutableJavaScriptSemanticGraph(resealed)).toBe(true);
|
|
} finally {
|
|
await reader.close();
|
|
}
|
|
});
|
|
|
|
const rejectInvalidTransfer = async (
|
|
wirePath: string,
|
|
descriptor: Awaited<ReturnType<typeof writeJavaScriptAnalysisTransfer>>,
|
|
file: JavaScriptArtifactFile,
|
|
wireRoot: string,
|
|
): Promise<void> => {
|
|
await expect(
|
|
readJavaScriptAnalysisTransfer(wirePath, descriptor, {
|
|
...file,
|
|
sha256: "0".repeat(64),
|
|
}),
|
|
).rejects.toThrow("different artifact source");
|
|
await expect(
|
|
readJavaScriptAnalysisTransfer(
|
|
wirePath,
|
|
{ ...descriptor, sha256: "0".repeat(64) },
|
|
file,
|
|
),
|
|
).rejects.toThrow("digest does not match");
|
|
const wire = await readFile(wirePath, "utf8");
|
|
const duplicate = wire
|
|
.split("\n")
|
|
.find((line) => line.startsWith('{"kind":"node"'));
|
|
if (duplicate === undefined)
|
|
throw new Error("Fixture emitted no semantic node record");
|
|
const invalid = `${wire}${duplicate}\n`;
|
|
const invalidPath = join(wireRoot, "duplicate.jsonl");
|
|
await writeFile(invalidPath, invalid);
|
|
await expect(
|
|
readJavaScriptAnalysisTransfer(
|
|
invalidPath,
|
|
{
|
|
...descriptor,
|
|
bytes: Buffer.byteLength(invalid),
|
|
records: descriptor.records + 1,
|
|
graph_bytes: descriptor.graph_bytes + Buffer.byteLength(duplicate) + 1,
|
|
graph_records: descriptor.graph_records + 1,
|
|
sha256: createHash("sha256").update(invalid).digest("hex"),
|
|
},
|
|
file,
|
|
),
|
|
).rejects.toThrow("Duplicate JavaScript transfer node identity");
|
|
};
|
|
|
|
it("reports semantic value resource limits in application graph coverage", async () => {
|
|
const root = await createTestTempDirectory("rea-javascript-semantic-limit-");
|
|
await writeFile(join(root, "app.js"), primitiveCandidateExpansionSource());
|
|
await writeFile(join(root, "growth.js"), primitiveByteExpansionSource());
|
|
const snapshot = await scanCanonicalArtifactInventory(root, {});
|
|
const reader = createJavaScriptArtifactReader(root, "directory");
|
|
try {
|
|
const files = await readJavaScriptArtifactFiles(reader, snapshot);
|
|
const analysis = analyzeJavaScriptArtifactFiles(files);
|
|
const graph = buildJavaScriptArtifactGraph(snapshot, files, analysis);
|
|
|
|
expect(graph.coverage).toMatchObject({
|
|
status: "partial",
|
|
omitted_count: null,
|
|
limits: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
name: "javascript_semantic_primitive_candidates",
|
|
unit: "items",
|
|
}),
|
|
expect.objectContaining({
|
|
name: "javascript_semantic_primitive_bytes",
|
|
unit: "bytes",
|
|
}),
|
|
]),
|
|
});
|
|
const sourceModuleLimits = graph.nodes
|
|
.filter(({ kind }) => kind === "javascript-module")
|
|
.flatMap(({ observations }) => observations)
|
|
.filter(
|
|
({ evidence }) =>
|
|
evidence.extractor.operation === "recover-source-module",
|
|
)
|
|
.flatMap(({ evidence }) => evidence.coverage.limits);
|
|
expect(sourceModuleLimits).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
name: "javascript_semantic_primitive_candidates",
|
|
unit: "items",
|
|
}),
|
|
expect.objectContaining({
|
|
name: "javascript_semantic_primitive_bytes",
|
|
unit: "bytes",
|
|
}),
|
|
]),
|
|
);
|
|
expect(graph.limitations).toContain(
|
|
"Primitive candidate budget exceeded (maximum 256 alternatives).",
|
|
);
|
|
expect(graph.limitations).toEqual(
|
|
expect.arrayContaining([
|
|
expect.stringMatching(/primitive string-byte budget exceeded/i),
|
|
]),
|
|
);
|
|
} finally {
|
|
await reader.close();
|
|
}
|
|
});
|
|
|
|
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,
|
|
});
|
|
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,
|
|
relevant_files: expect.any(Number),
|
|
text_bytes_read: expect.any(Number),
|
|
});
|
|
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-v2",
|
|
}),
|
|
}),
|
|
]),
|
|
}),
|
|
]),
|
|
);
|
|
expect(graph.edges.map(({ relation }) => relation)).toEqual(
|
|
expect.arrayContaining([
|
|
"contains",
|
|
"loads",
|
|
"imports",
|
|
"maps_to",
|
|
"exposes",
|
|
"calls",
|
|
"persists_to",
|
|
]),
|
|
);
|
|
assertSemanticLinks(result, graph, semanticGraph);
|
|
});
|
|
|
|
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",
|
|
});
|
|
const second = await reconstructJavaScriptArtifact({
|
|
input_path: archive,
|
|
format: "asar",
|
|
});
|
|
|
|
expect(first.graph).toEqual(second.graph);
|
|
expect(first.inventory_graph_sha256).toBe(second.inventory_graph_sha256);
|
|
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 suffix-named ASAR directories in the graph and analyzes their files", async () => {
|
|
const root = await createTestTempDirectory(
|
|
"rea-javascript-asar-suffix-directory-",
|
|
);
|
|
const source = join(root, "source");
|
|
const zipDirectory = join(source, "node_modules", "@zip.js");
|
|
await mkdir(zipDirectory, { recursive: true });
|
|
await writeJavaScriptArtifactFixture(source);
|
|
await writeFile(
|
|
join(zipDirectory, "entry.js"),
|
|
"export const packed = true;\n",
|
|
);
|
|
const archive = join(root, "app.asar");
|
|
await createPackageWithOptions(source, archive, {});
|
|
|
|
const snapshot = await scanArtifactInventory(archive);
|
|
const zipDirectoryOccurrence = snapshot.occurrences.find(
|
|
({ logical_path }) => logical_path === "node_modules/@zip.js",
|
|
);
|
|
expect(zipDirectoryOccurrence).toMatchObject({
|
|
entry_kind: "directory",
|
|
artifact_id: expect.stringMatching(/^art_[a-f0-9]{64}$/u),
|
|
});
|
|
expect(
|
|
snapshot.nodes.some(
|
|
({ artifact_id }) => artifact_id === zipDirectoryOccurrence?.artifact_id,
|
|
),
|
|
).toBe(true);
|
|
|
|
await reconstructAsarArchive(archive, [
|
|
expect.objectContaining({
|
|
kind: "javascript-asset",
|
|
observations: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
properties: expect.objectContaining({
|
|
path: "node_modules/@zip.js/entry.js",
|
|
}),
|
|
}),
|
|
]),
|
|
}),
|
|
]);
|
|
});
|
|
|
|
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);
|
|
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.",
|
|
]),
|
|
});
|
|
if (missingNative === undefined)
|
|
throw new Error("Inventory omitted the unavailable native companion");
|
|
|
|
const result = await reconstructAsarArchive(archive, [
|
|
expect.objectContaining({ kind: "javascript-asset" }),
|
|
]);
|
|
const missingEntry = result.graph.nodes.find(
|
|
({ kind, observations }) =>
|
|
kind === "asar-entry" &&
|
|
observations.some(({ label }) => label === missingNative.logical_path),
|
|
);
|
|
expect(missingEntry).toMatchObject({
|
|
identity: {
|
|
strategy: "canonical-path",
|
|
stability: "artifact-version",
|
|
artifact_sha256: snapshot.manifest.root_sha256,
|
|
path: missingNative.logical_path,
|
|
},
|
|
observations: [
|
|
expect.objectContaining({
|
|
label: missingNative.logical_path,
|
|
properties: expect.objectContaining({
|
|
entry_sha256: null,
|
|
bytes: missingNative.declared_size,
|
|
inventory_artifact_id: null,
|
|
inventory_occurrence_id: missingNative.occurrence_id,
|
|
hash_status: "unavailable",
|
|
artifact_kind: "native-addon",
|
|
kind: "native-addon",
|
|
content_available: false,
|
|
}),
|
|
evidence: expect.objectContaining({
|
|
limitations: expect.arrayContaining([
|
|
"ASAR unpacked companion bytes were unavailable; no content hash or child artifact was produced.",
|
|
"The inventoried native-addon entry has unavailable bytes; no content artifact identity can be established.",
|
|
]),
|
|
}),
|
|
}),
|
|
],
|
|
});
|
|
const unavailableAddon = result.graph.nodes.find(
|
|
({ kind, observations }) =>
|
|
kind === "native-addon" &&
|
|
observations.some(({ label }) => label === missingNative.logical_path),
|
|
);
|
|
expect(unavailableAddon).toMatchObject({
|
|
identity: {
|
|
strategy: "canonical-path",
|
|
stability: "artifact-version",
|
|
artifact_sha256: snapshot.manifest.root_sha256,
|
|
path: missingNative.logical_path,
|
|
},
|
|
observations: [
|
|
expect.objectContaining({
|
|
label: missingNative.logical_path,
|
|
properties: expect.objectContaining({
|
|
availability: "unavailable",
|
|
hash_status: "unavailable",
|
|
artifact_kind: "native-addon",
|
|
bytes: missingNative.declared_size,
|
|
inventory_artifact_id: null,
|
|
inventory_occurrence_id: missingNative.occurrence_id,
|
|
}),
|
|
}),
|
|
],
|
|
});
|
|
});
|
|
|
|
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,
|
|
});
|
|
|
|
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",
|
|
}),
|
|
}),
|
|
]),
|
|
}),
|
|
]),
|
|
);
|
|
});
|
|
|
|
const assertSemanticLinks = (
|
|
result: Awaited<ReturnType<typeof reconstructJavaScriptArtifact>>,
|
|
graph: ReturnType<typeof parseJavaScriptApplicationGraph>,
|
|
semanticGraph: ReturnType<typeof parseJavaScriptSemanticGraph>,
|
|
) => {
|
|
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(() =>
|
|
javascriptApplicationAnalysisResultSchema.parse({
|
|
...analysisResult,
|
|
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 roles = ["electron-main", "electron-preload"].map((kind) =>
|
|
graph.nodes.find((node) => node.kind === kind),
|
|
);
|
|
expect(roles[0]?.observations[0]?.properties).toMatchObject({
|
|
declared_path: "main.js",
|
|
resolution_context: "package-entrypoint",
|
|
resolved_path: "main.js",
|
|
resolution_status: "resolved",
|
|
limitations: [],
|
|
});
|
|
expect(roles[1]?.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 roles) {
|
|
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=fixture-secret");
|
|
};
|
|
|
|
const reconstructAsarArchive = async (
|
|
archive: string,
|
|
expectedNodes: readonly unknown[],
|
|
): Promise<Awaited<ReturnType<typeof reconstructJavaScriptArtifact>>> => {
|
|
const result = await reconstructJavaScriptArtifact({
|
|
input_path: archive,
|
|
format: "asar",
|
|
});
|
|
expect(result.statistics.parsed_javascript_files).toBeGreaterThan(0);
|
|
expect(result.graph.nodes).toEqual(
|
|
expect.arrayContaining([...expectedNodes]),
|
|
);
|
|
return result;
|
|
};
|
|
|
|
const fixtureDirectory = async (): Promise<string> => {
|
|
const root = await createTestTempDirectory("rea-javascript-artifact-");
|
|
await writeJavaScriptArtifactFixture(root);
|
|
return root;
|
|
};
|
|
|
|
for (const extension of ["ts", "tsx", "mts", "cts", "js", "mjs", "cjs"]) {
|
|
it.each(["directory", "asar"] as const)(
|
|
`retains ${extension} source facts from a real %s artifact`,
|
|
async (format) => {
|
|
const root = await createTestTempDirectory("rea-nodenext-artifacts-");
|
|
const directory = join(root, "source");
|
|
await mkdir(directory);
|
|
const path = `selected.${extension}`;
|
|
const parameter = ["ts", "tsx", "mts", "cts"].includes(extension)
|
|
? "value: string"
|
|
: "value";
|
|
const source = `
|
|
globalThis.__rea_nodenext_executed = true;
|
|
throw new Error("fixture must remain inert");
|
|
export function selected_feature(${parameter}) { return { value }; }
|
|
`;
|
|
await writeFile(join(directory, path), source);
|
|
let inputPath = directory;
|
|
if (format === "asar") {
|
|
inputPath = join(root, "application.asar");
|
|
await createPackageWithOptions(directory, inputPath, {});
|
|
}
|
|
Reflect.deleteProperty(globalThis, "__rea_nodenext_executed");
|
|
const result = await reconstructJavaScriptArtifact({
|
|
input_path: inputPath,
|
|
format,
|
|
});
|
|
expect(
|
|
Reflect.get(globalThis, "__rea_nodenext_executed"),
|
|
).toBeUndefined();
|
|
expect(result.statistics).toMatchObject({
|
|
relevant_files: 1,
|
|
parsed_javascript_files: 1,
|
|
text_bytes_read: Buffer.byteLength(source),
|
|
parse_failures: 0,
|
|
});
|
|
const asset = result.graph.nodes.find(
|
|
({ kind, observations }) =>
|
|
kind === "javascript-asset" &&
|
|
observations.some(({ properties }) => properties.path === path),
|
|
);
|
|
expect(asset?.identity).toMatchObject({
|
|
sha256: createHash("sha256").update(source).digest("hex"),
|
|
});
|
|
expect(asset?.observations[0]?.properties).toMatchObject({
|
|
path,
|
|
bytes: Buffer.byteLength(source),
|
|
file_kind: "javascript",
|
|
parse_status: "complete",
|
|
});
|
|
expect(
|
|
result.semantic_graph.nodes.some(
|
|
({ identity }) => identity.module_path === path,
|
|
),
|
|
).toBe(true);
|
|
expect(result.graph.coverage).toMatchObject({
|
|
status: "complete",
|
|
truncated: false,
|
|
});
|
|
},
|
|
);
|
|
}
|