Files
rea/tests/acceptance/applications/javascriptSourceSelectionCli.test.ts
T
morluto d81d6b6707 fix(javascript): preserve semantic gaps for unparseable sources
Successful worker transport can carry an explicit failed parser status
with no semantic projection. Preserve that producer fact through the
shared source-availability interpretation used by full and incremental
graph construction, rather than dropping the source's semantic scope.

Retain the module when capacity permits, per-family incomplete-module
unknowns, and unknown omitted extents. Preserve existing operational
failure reasons and limits without classifying syntax as resource loss.
Document this coverage behavior.

Validation: regression-first run had 3 expected failures; all 22 selected
projection and public CLI source-selection/HTML cases passed after repair.
No JavaScript target execution or live external provider was involved.
2026-10-24 08:58:22 +08:00

259 lines
8.4 KiB
TypeScript

import { createPackageWithOptions } from "@electron/asar";
import { createHash } from "node:crypto";
import { readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect } from "vitest";
import { parseEvidence } from "../../../src/domain/evidence.js";
import { javascriptApplicationAnalysisResultSchema } from "../../../src/domain/javascript/javascriptApplicationAnalysis.js";
import { JAVASCRIPT_SEMANTIC_RELATION_FAMILIES } from "../../../src/domain/javascript/javascriptSemanticGraphSchemas.js";
import { writeJavaScriptSourceSelectionFixture } from "../../fixtures/javascriptSourceSelection.js";
import {
asarHeaderBytes,
prependAsarJsonWhitespace,
} from "../../fixtures/asarHeader.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { cliTest } from "../../support/cli/cliFixture.js";
cliTest.for([
{ format: "directory", command: "analyze" },
{ format: "asar", command: "analyze-javascript-application" },
])(
"analyzes selected $format sources through $command with complete artifact identity",
{ timeout: 60_000 },
async ({ format, command }, { cli }) => {
const directory = await createTestTempDirectory("rea-js-source-selection-");
const files = await writeJavaScriptSourceSelectionFixture(directory);
let input = directory;
if (format === "asar") {
input = join(
await createTestTempDirectory("rea-js-selected-asar-"),
"app.asar",
);
await createPackageWithOptions(directory, input, {});
}
const requested = ["main.mjs", "package.json", "main.mjs"];
const response = await cli.run({
arguments: [
command,
input,
...requested.flatMap((path) => ["--source-path", path]),
"--format",
"json",
],
});
expect(response.exitCode).toBe(0);
const evidence = parseEvidence(response.json);
const result = javascriptApplicationAnalysisResultSchema.parse(
evidence.normalized_result,
);
expect(result.source_selection).toEqual({
requested_paths: requested,
unselected_files: 4,
});
expect(evidence.parameters.source_paths).toEqual(requested);
expect(result.statistics).toMatchObject({
relevant_files: 6,
parsed_javascript_files: 1,
invalid_utf8_files: 0,
parse_failures: 0,
text_bytes_read:
files.get("main.mjs")!.length + files.get("package.json")!.length,
});
expect(result.graph.coverage.status).toBe("partial");
expect(result.semantic_graph.coverage).toMatchObject({
status: "partial",
truncated: false,
});
expect(
result.semantic_graph.nodes.some(
({ kind, label }) => kind === "function" && label === "selected",
),
).toBe(true);
expect(
result.semantic_graph.unknowns.some(
({ reason, detail }) =>
reason === "incomplete-module" &&
detail.includes("caller-selected source_paths"),
),
).toBe(true);
for (const [path, bytes] of files) {
const properties = result.graph.nodes
.flatMap(({ observations }) =>
observations.map(({ properties }) => properties),
)
.find(
(properties) =>
properties.path === path && properties.text_status !== undefined,
);
expect(properties?.text_status).toBe(
requested.includes(path) ? "included" : "outside-analysis-scope",
);
expect(properties?.parse_status).toBe(
path === "main.mjs" ? "complete" : null,
);
expect(await readFile(join(directory, path))).toEqual(bytes);
}
const fullResponse = await cli.run({
arguments: [command, input, "--format", "json"],
});
expect(fullResponse.exitCode).toBe(0);
const full = javascriptApplicationAnalysisResultSchema.parse(
parseEvidence(fullResponse.json).normalized_result,
);
expect(full.source_selection).toBeUndefined();
expect(full.statistics.invalid_utf8_files).toBe(1);
const unavailableModule = full.semantic_graph.nodes.find(
({ kind, identity }) =>
kind === "module" && identity.module_path === "invalid.mjs",
);
expect(unavailableModule).toBeDefined();
expect(
full.semantic_graph.unknowns.filter(
({ node_id, detail }) =>
node_id === unavailableModule?.node_id &&
detail.includes("not valid UTF-8"),
),
).toHaveLength(JAVASCRIPT_SEMANTIC_RELATION_FAMILIES.length);
const unparsedModule = full.semantic_graph.nodes.find(
({ kind, identity }) =>
kind === "module" && identity.module_path === "vendor.js",
);
expect(unparsedModule).toBeDefined();
expect(
full.semantic_graph.unknowns.filter(
({ node_id, detail, reason }) =>
node_id === unparsedModule?.node_id &&
reason === "incomplete-module" &&
detail.includes("syntax could not be parsed"),
),
).toHaveLength(JAVASCRIPT_SEMANTIC_RELATION_FAMILIES.length);
expect(full.semantic_graph.coverage).toMatchObject({
status: "partial",
truncated: false,
omitted_nodes: null,
omitted_relations: null,
});
expect(full.statistics.parse_failures).toBeGreaterThan(0);
expect([
result.root_artifact_sha256,
result.inventory_manifest_id,
result.inventory_graph_sha256,
]).toEqual([
full.root_artifact_sha256,
full.inventory_manifest_id,
full.inventory_graph_sha256,
]);
if (format === "asar")
expect(result.root_artifact_sha256).toBe(
createHash("sha256")
.update(await readFile(input))
.digest("hex"),
);
},
);
cliTest(
"rejects source selection when an explicit provider chooses the native lane",
async ({ cli }) => {
const directory = await createTestTempDirectory(
"rea-js-source-native-refusal-",
);
await writeJavaScriptSourceSelectionFixture(directory);
const response = await cli.run({
arguments: [
"analyze",
directory,
"--provider",
"ghidra",
"--source-path",
"main.mjs",
"--format",
"json",
],
});
expect(response.exitCode).toBe(1);
expect(response.json).toMatchObject({
code: "invalid_request",
details: {
issues: [{ path: ["source_paths"], reason: "invalid_value" }],
},
});
},
);
cliTest(
"accepts legal ASAR JSON whitespace while preserving selected-source identity",
{ timeout: 60_000 },
async ({ cli }) => {
const root = await createTestTempDirectory("rea-js-asar-whitespace-");
await writeJavaScriptSourceSelectionFixture(root);
const archive = join(
await createTestTempDirectory("rea-js-asar-whitespace-output-"),
"app.asar",
);
await createPackageWithOptions(root, archive, {});
for (const prefix of [" \t\r\n", " ".repeat(9_001)]) {
await prependAsarJsonWhitespace(archive, prefix);
const response = await cli.run({
arguments: [
"analyze-javascript-application",
archive,
"--source-path",
"main.mjs",
"--format",
"json",
],
});
expect(response.exitCode).toBe(0);
const evidence = parseEvidence(response.json);
const result = javascriptApplicationAnalysisResultSchema.parse(
evidence.normalized_result,
);
expect(result.statistics).toMatchObject({
nested_asar_containers: 0,
parsed_javascript_files: 1,
parse_failures: 0,
});
expect(result.semantic_graph.nodes).toContainEqual(
expect.objectContaining({ kind: "function", label: "selected" }),
);
expect(result.root_artifact_sha256).toBe(
createHash("sha256")
.update(await readFile(archive))
.digest("hex"),
);
}
},
);
cliTest(
"does not parse ASAR-looking JSON beyond the declared JSON extent",
async ({ cli }) => {
const archive = join(
await createTestTempDirectory("rea-js-asar-padding-output-"),
"opaque.asar",
);
await writeFile(
archive,
asarHeaderBytes(" ", {
padding: Buffer.from('{"files":{"main.mjs":{"size":0,"offset":"0"}}}'),
}),
);
const response = await cli.run({
arguments: [
"analyze-javascript-application",
archive,
"--format",
"json",
],
});
expect(response.exitCode).toBe(1);
expect(response.json).toMatchObject({
code: "artifact_operation_failed",
details: { reason: "format" },
});
},
);