Files
rea/src/application/javascript/JavaScriptArtifactAnalysis.order.test.ts
T
512bcf5cdb fix(javascript): analyze first-party sources before dependencies (#9341)
* fix(javascript): analyze first-party sources before dependencies

Every source in one application analysis charges the same parent result
budget, and sources were offered in lexical inventory order. On an Electron
app.asar, node_modules/ sorts before an electron-vite out-*/ directory, so
dependency files spent the budget and every first-party main, preload, and
renderer file had its semantic analysis not started and its application
projection rejected. The summary then reported zero windows, preloads, and
bridges while electron_semantics still said "recovered".

Offer application JavaScript before JavaScript under node_modules; reported
files keep inventory order. Count Electron boundary facts only from files
whose projection was admitted, and report electron_semantics "partial" (with
a limitation naming the count) when an Electron-loading source was not fully
examined or reported.

Fixes #9321

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(javascript): treat limited Electron examination as partial coverage

A static pass can stop early at the Electron wrapper expansion budget or
another resource limit without failing the worker. Such a source carries no
analysis or projection failure, so its retained IPC or bridge facts still
reported electron_semantics "recovered". Read the owned examination status
and count a limited Electron-loading source as incomplete.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: morluto <[email protected]>
2026-11-09 19:42:19 +08:00

159 lines
5.3 KiB
TypeScript

import { expect, it } from "vitest";
import {
analyzeAndAdoptJavaScriptArtifactFiles,
javaScriptAnalysisOrder,
} from "./JavaScriptArtifactAnalysis.js";
import { createJavaScriptSemanticGraphProjection } from "./JavaScriptSemanticGraphBuilder.js";
import { AnalysisResourceConstraintError } from "../../domain/analysisErrorCore.js";
import { analyzeJavaScriptStaticSource } from "../../domain/javascript/javascriptStaticAnalysis.js";
import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js";
import { err, ok } from "../../domain/result.js";
const source = (path: string): JavaScriptArtifactFile => ({
path,
container_sha256: "a".repeat(64),
sha256: "b".repeat(64),
bytes: 20,
inventory_artifact_id: path,
kind: "javascript",
unpacked: false,
text: { included: true, value: "export const x = 1;" },
});
const manifest = (path: string): JavaScriptArtifactFile => ({
...source(path),
kind: "package-json",
text: { included: true, value: "{}" },
});
// Lexical inventory order puts `node_modules/` before an electron-vite `out-*`
// directory, which is the shape that exhausted the result budget in #9321.
const inventory = [
source("node_modules/a/index.js"),
manifest("node_modules/a/package.json"),
source("node_modules/b/lib/yaml.js"),
source("out-global-online/main/index.js"),
source("out-global-online/preload/index.js"),
manifest("package.json"),
];
it("offers application JavaScript before dependency JavaScript", () => {
expect(javaScriptAnalysisOrder(inventory).map(({ path }) => path)).toEqual([
"node_modules/a/package.json",
"out-global-online/main/index.js",
"out-global-online/preload/index.js",
"package.json",
"node_modules/a/index.js",
"node_modules/b/lib/yaml.js",
]);
// A package merely named like a dependency directory is not one.
expect(
javaScriptAnalysisOrder([
source("node_modules.js"),
source("src/node_modules_shim.js"),
]).map(({ path }) => path),
).toEqual(["node_modules.js", "src/node_modules_shim.js"]);
});
it("spends a limited result budget on first-party sources and keeps inventory order", async () => {
const javascript = analyzeJavaScriptStaticSource("export const x = 1;");
const analyzed: string[] = [];
// Admit two sources, then report the budget as spent, as the parent does
// once its remaining result capacity is reserved for static observations.
const worker = {
analyze: async (file: JavaScriptArtifactFile) => {
analyzed.push(file.path);
const value = { javascript, module: null, projection: null };
if (analyzed.length <= 2) return ok(value);
return err({
...value,
error: new AnalysisResourceConstraintError(
"analyze_javascript_application",
"memory",
`semantic analysis of ${file.path} was not started`,
{ semantic_result_budget_bytes: 0 },
),
});
},
};
const outcome = await analyzeAndAdoptJavaScriptArtifactFiles(
{
files: inventory,
unavailable_relevant_files: [],
containers: [],
text_bytes_read: 120,
invalid_utf8_files: 0,
},
worker,
createJavaScriptSemanticGraphProjection(),
async () => {},
);
expect(analyzed).toEqual([
"out-global-online/main/index.js",
"out-global-online/preload/index.js",
"node_modules/a/index.js",
"node_modules/b/lib/yaml.js",
]);
expect(outcome.analysis.files.map(({ file }) => file.path)).toEqual(
inventory.map(({ path }) => path),
);
const failed = outcome.analysis.files
.filter(({ analysis_failure }) => analysis_failure !== undefined)
.map(({ file }) => file.path);
expect(failed).toEqual([
"node_modules/a/index.js",
"node_modules/b/lib/yaml.js",
]);
expect(outcome.interruption).toBeNull();
});
it("spends capacity in the caller's requested order for explicitly selected sources", async () => {
const files = [
source("node_modules/selected/index.js"),
source("src/main.js"),
];
const admitted: string[] = [];
const javascript = analyzeJavaScriptStaticSource("export const x = 1;");
const outcome = await analyzeAndAdoptJavaScriptArtifactFiles(
{
files,
unavailable_relevant_files: [],
containers: [],
text_bytes_read: 40,
invalid_utf8_files: 0,
source_selection: {
requested_paths: files.map((file) => file.path),
unselected_files: 0,
},
},
{
analyze: async (file) => {
const value = { javascript, module: null, projection: null };
admitted.push(file.path);
return admitted.length === 1
? ok(value)
: err({
...value,
error: new AnalysisResourceConstraintError(
"analyze_javascript_application",
"memory",
"result capacity exhausted",
{ semantic_result_budget_bytes: 0 },
),
});
},
},
createJavaScriptSemanticGraphProjection(),
async () => {},
);
expect(admitted).toEqual(files.map((file) => file.path));
expect(
outcome.analysis.files.find(({ file }) => file.path === files[0]!.path)
?.analysis_failure,
).toBeUndefined();
expect(
outcome.analysis.files.find(({ file }) => file.path === files[1]!.path)
?.analysis_failure,
).toBeDefined();
});