* fix(javascript): name heap and semantic ceilings Refs #5910. Heap exhaustion names max_heap_mb with its effective value, range, and next-analysis worker restart. Semantic ceiling coverage names the four max_semantic_* arguments. Wakaru and transfer ceilings say this release has no runtime override. Co-authored-by: morluto <morluto@users.noreply.github.com> * fix(javascript): distinguish adjustable ceilings from fixed limits The effective transfer file ceiling and the parent result budget follow the CLI/MCP process heap, so recovery names NODE_OPTIONS instead of denying an override. A provenance report over 8 MiB keeps the fixed allowance guidance, and the Wakaru deadline says an unchanged retry may hit it again. Refs #5910 Co-authored-by: morluto <morluto@users.noreply.github.com> * fix(javascript): keep Bun OOM and checkpoint heap controls distinct Bun heap-exhaustion exits no longer claim the ignored max_heap_mb V8 old-space limit. Checkpoint decode names the parent heap and Node NODE_OPTIONS, the same control as the parent result budget. Refs #5910 Co-authored-by: morluto <morluto@users.noreply.github.com> * fix(javascript): name semantic ceilings without claiming both were reached Truncated semantic retention names the total and per-file controls as effective ceilings. Callers only know that retention was truncated, so the limitation no longer says both ceilings were reached. Refs #5910 Co-authored-by: morluto <morluto@users.noreply.github.com> * fix(javascript): name only the semantic ceiling that stopped retention Semantic ceiling limitations claimed both the application total and the per-file control whenever either one truncated the graph. Record which offered budget stopped node and unknown retention, and name only those controls. Coverage limits still list both effective values. Co-authored-by: morluto <morluto@users.noreply.github.com> * fix(javascript): require a value in the NODE_OPTIONS heap example A bare --max-old-space-size is an illegal V8 flag and prevents Node from starting. Parent-heap and transfer-ceiling recovery now share --max-old-space-size=<MiB>, and the Kuna heap remediation uses the same complete form. Co-authored-by: morluto <morluto@users.noreply.github.com> * fix(javascript): limit the old-space placeholder to NODE_OPTIONS recovery The review covers the CLI/MCP NODE_OPTIONS example and the transfer-ceiling copy. The Kuna heap-headroom sentence stays unchanged. Co-authored-by: morluto <morluto@users.noreply.github.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: morluto <morluto@users.noreply.github.com>
221 lines
6.8 KiB
TypeScript
221 lines
6.8 KiB
TypeScript
import { writeFile } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
|
|
import { expect } from "vitest";
|
|
import { z } from "zod";
|
|
|
|
import { javascriptApplicationAnalysisResultSchema } from "../../../src/domain/javascript/javascriptApplicationAnalysis.js";
|
|
import { analyzeJavaScriptApplication } from "../../support/javascriptApplicationScope.js";
|
|
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
|
import { cliTest } from "../../support/cli/cliFixture.js";
|
|
|
|
const filter = "evidence_id,normalized_result.graph.nodes[0,1].node_id";
|
|
|
|
for (const scenario of [
|
|
{
|
|
command: "analyze-javascript-application",
|
|
format: "json",
|
|
fullOutput: false,
|
|
filtered: true,
|
|
},
|
|
{
|
|
command: "analyze-javascript-application",
|
|
format: "jsonl",
|
|
fullOutput: true,
|
|
filtered: true,
|
|
},
|
|
{ command: "analyze", format: "json", fullOutput: true, filtered: false },
|
|
{ command: "analyze", format: "jsonl", fullOutput: false, filtered: false },
|
|
] as const) {
|
|
cliTest(
|
|
`${scenario.command} preserves representative ${scenario.format} output`,
|
|
async ({ cli }) => {
|
|
const root = await createTestTempDirectory("rea-js-json-output-");
|
|
await writeFile(join(root, "main.js"), "fetch('');\n");
|
|
const direct = await analyzeJavaScriptApplication({ input_path: root });
|
|
if (!direct.ok) throw direct.error;
|
|
const normalized = javascriptApplicationAnalysisResultSchema.parse(
|
|
direct.value.normalized_result,
|
|
);
|
|
const output = await cli.run({
|
|
arguments: [
|
|
scenario.command,
|
|
root,
|
|
"--format",
|
|
scenario.format,
|
|
...(scenario.fullOutput ? ["--full-output"] : []),
|
|
...(scenario.filtered ? ["--filter-output", filter] : []),
|
|
],
|
|
});
|
|
expect(output.exitCode).toBe(0);
|
|
let data: unknown = output.json;
|
|
if (scenario.fullOutput) {
|
|
const envelope = z
|
|
.object({
|
|
ok: z.literal(true),
|
|
data: z.unknown(),
|
|
meta: z.object({
|
|
command: z.literal(scenario.command),
|
|
duration: z.string().regex(/^\d+ms$/u),
|
|
}),
|
|
})
|
|
.parse(output.json);
|
|
data = envelope.data;
|
|
}
|
|
expect(data).toEqual(
|
|
scenario.filtered
|
|
? {
|
|
evidence_id: direct.value.evidence_id,
|
|
normalized_result: {
|
|
graph: {
|
|
nodes: normalized.graph.nodes
|
|
.slice(0, 1)
|
|
.map(({ node_id }) => ({ node_id })),
|
|
},
|
|
},
|
|
}
|
|
: direct.value,
|
|
);
|
|
if (scenario.filtered)
|
|
expect(normalized.graph.nodes.length).toBeGreaterThan(0);
|
|
},
|
|
);
|
|
}
|
|
|
|
cliTest(
|
|
"preserves a complete empty filtered JSON document",
|
|
async ({ cli }) => {
|
|
const root = await createTestTempDirectory("rea-js-json-empty-filter-");
|
|
await writeFile(join(root, "main.js"), "export const value = 1;\n");
|
|
const output = await cli.run({
|
|
arguments: [
|
|
"analyze-javascript-application",
|
|
root,
|
|
"--json",
|
|
"--filter-output",
|
|
"absent.field",
|
|
],
|
|
});
|
|
expect(output.exitCode).toBe(0);
|
|
expect(output.json).toEqual({});
|
|
},
|
|
);
|
|
|
|
cliTest(
|
|
"preserves a typed failed analysis in the JSON result",
|
|
async ({ cli }) => {
|
|
const root = await createTestTempDirectory("rea-js-json-failure-");
|
|
const output = await cli.run({
|
|
arguments: [
|
|
"analyze-javascript-application",
|
|
join(root, "absent"),
|
|
"--json",
|
|
],
|
|
});
|
|
expect(output.json).toMatchObject({
|
|
code: "invalid_request",
|
|
details: { issues: [{ path: ["input_path"], reason: "invalid_value" }] },
|
|
});
|
|
},
|
|
);
|
|
|
|
cliTest(
|
|
"preserves JSON omission for inherited filter methods",
|
|
async ({ cli }) => {
|
|
const root = await createTestTempDirectory("rea-js-json-inherited-filter-");
|
|
await writeFile(join(root, "main.js"), "export const value = 1;\n");
|
|
const output = await cli.run({
|
|
arguments: [
|
|
"analyze-javascript-application",
|
|
root,
|
|
"--json",
|
|
"--filter-output",
|
|
"toString",
|
|
],
|
|
});
|
|
expect(output.exitCode).toBe(0);
|
|
expect(output.json).toEqual({});
|
|
},
|
|
);
|
|
|
|
cliTest(
|
|
"honors caller-selected semantic node limits across files and records them in Evidence",
|
|
async ({ cli }) => {
|
|
const root = await createTestTempDirectory("rea-js-node-budgets-");
|
|
await writeFile(join(root, "first.js"), "external;\n".repeat(40));
|
|
await writeFile(join(root, "second.js"), "another;\n".repeat(40));
|
|
const output = await cli.run({
|
|
arguments: [
|
|
"analyze-javascript-application",
|
|
root,
|
|
"--format",
|
|
"json",
|
|
"--max-semantic-nodes",
|
|
"50",
|
|
"--max-semantic-nodes-per-file",
|
|
"30",
|
|
],
|
|
});
|
|
expect(output.exitCode).toBe(0);
|
|
const result = z
|
|
.object({
|
|
parameters: z.object({
|
|
max_semantic_nodes: z.number(),
|
|
max_semantic_nodes_per_file: z.number(),
|
|
}),
|
|
normalized_result: z.object({
|
|
semantic_graph: z.object({
|
|
nodes: z.array(
|
|
z.object({ identity: z.object({ module_path: z.string() }) }),
|
|
),
|
|
coverage: z.object({
|
|
truncated: z.boolean(),
|
|
omitted_nodes: z.number().nullable(),
|
|
limits: z.array(
|
|
z.object({ name: z.string(), value: z.number() }),
|
|
),
|
|
}),
|
|
}),
|
|
}),
|
|
})
|
|
.parse(output.json);
|
|
expect(result.parameters).toEqual({
|
|
max_semantic_nodes: 50,
|
|
max_semantic_nodes_per_file: 30,
|
|
});
|
|
const graph = result.normalized_result.semantic_graph;
|
|
expect(graph.nodes).toHaveLength(50);
|
|
expect(
|
|
graph.nodes.filter((node) => node.identity.module_path === "first.js"),
|
|
).toHaveLength(30);
|
|
expect(
|
|
graph.nodes.filter((node) => node.identity.module_path === "second.js"),
|
|
).toHaveLength(20);
|
|
expect(graph.coverage.truncated).toBe(true);
|
|
expect(graph.coverage.omitted_nodes).toBeNull();
|
|
expect(graph.coverage.limits).toEqual(
|
|
expect.arrayContaining([
|
|
{ name: "semantic_graph_node_ceiling", value: 50 },
|
|
{ name: "semantic_graph_file_node_ceiling", value: 30 },
|
|
{ name: "max_semantic_nodes", value: 50 },
|
|
{ name: "max_semantic_nodes_per_file", value: 30 },
|
|
]),
|
|
);
|
|
const limitation = z
|
|
.object({
|
|
normalized_result: z.object({
|
|
semantic_graph: z.object({
|
|
limitations: z.array(z.string()),
|
|
}),
|
|
}),
|
|
})
|
|
.parse(output.json);
|
|
expect(
|
|
limitation.normalized_result.semantic_graph.limitations.join("\n"),
|
|
).toContain("max_semantic_nodes");
|
|
expect(
|
|
limitation.normalized_result.semantic_graph.limitations.join("\n"),
|
|
).toContain("1 to 1000000");
|
|
},
|
|
);
|