mirror of
https://github.com/morluto/rea.git
synced 2026-10-29 10:55:57 +00:00
* 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>
225 lines
8.6 KiB
TypeScript
225 lines
8.6 KiB
TypeScript
import { expect, it } from "vitest";
|
|
import { AnalysisResourceConstraintError } from "../analysisErrorCore.js";
|
|
import { projectAnalysisError } from "../analysisErrorProjection.js";
|
|
import {
|
|
JAVASCRIPT_ANALYSIS_RESOURCE_DEFAULTS,
|
|
JAVASCRIPT_PARENT_CHECKPOINT_HEAP_REMEDIATION,
|
|
JAVASCRIPT_PARENT_RESULT_BUDGET_REMEDIATION,
|
|
JAVASCRIPT_SEMANTIC_ITEM_MAXIMUM,
|
|
JAVASCRIPT_WORKER_HEAP_MIB_MAXIMUM,
|
|
JAVASCRIPT_WORKER_HEAP_MIB_MINIMUM,
|
|
javaScriptAnalysisResourceControlsSchema,
|
|
javaScriptWorkerHeapExhaustionRecovery,
|
|
javaScriptSemanticCeilingCoverage,
|
|
javaScriptWorkerHeapRemediation,
|
|
} from "./javascriptAnalysisResourceControls.js";
|
|
|
|
it("keeps omitted operational choices absent and validates explicit semantic budgets", () => {
|
|
expect(javaScriptAnalysisResourceControlsSchema.parse({})).toEqual({});
|
|
expect(
|
|
javaScriptAnalysisResourceControlsSchema.parse({
|
|
max_semantic_nodes: 750_000,
|
|
max_semantic_nodes_per_file: 700_000,
|
|
max_semantic_unknowns: 500_000,
|
|
max_semantic_unknowns_per_file: 400_000,
|
|
}),
|
|
).toEqual({
|
|
max_semantic_nodes: 750_000,
|
|
max_semantic_nodes_per_file: 700_000,
|
|
max_semantic_unknowns: 500_000,
|
|
max_semantic_unknowns_per_file: 400_000,
|
|
});
|
|
for (const value of [0, -1, 1.5, 1_000_001, Infinity, NaN]) {
|
|
for (const key of [
|
|
"max_semantic_nodes",
|
|
"max_semantic_nodes_per_file",
|
|
"max_semantic_unknowns",
|
|
"max_semantic_unknowns_per_file",
|
|
])
|
|
expect(
|
|
javaScriptAnalysisResourceControlsSchema.safeParse({ [key]: value })
|
|
.success,
|
|
).toBe(false);
|
|
}
|
|
});
|
|
|
|
it("names max_heap_mb, its range, and the budgets it does not raise", () => {
|
|
const heapMb = JAVASCRIPT_ANALYSIS_RESOURCE_DEFAULTS.heapMb;
|
|
const error = new AnalysisResourceConstraintError(
|
|
"analyze_javascript_application",
|
|
"memory",
|
|
`Owned JavaScript worker exhausted its ${String(heapMb)} MiB V8 old-space heap (max_heap_mb) while analyzing app.js during static`,
|
|
{ worker_old_space_mb: heapMb },
|
|
{ remediationAction: javaScriptWorkerHeapRemediation(heapMb) },
|
|
);
|
|
const projected = projectAnalysisError(error);
|
|
expect(projected.message).toContain(`max_heap_mb`);
|
|
expect(projected.message).toContain(`${String(heapMb)} MiB`);
|
|
expect(projected.remediation.action).toContain("max_heap_mb");
|
|
expect(projected.remediation.action).toContain(
|
|
String(JAVASCRIPT_WORKER_HEAP_MIB_MINIMUM),
|
|
);
|
|
expect(projected.remediation.action).toContain(
|
|
String(JAVASCRIPT_WORKER_HEAP_MIB_MAXIMUM),
|
|
);
|
|
expect(projected.remediation.action).toContain("--max-heap-mb");
|
|
expect(projected.remediation.action).toContain("next analysis");
|
|
expect(projected.remediation.action).toContain("new worker");
|
|
expect(projected.remediation.action).toContain("parent result budget");
|
|
expect(projected.remediation.action).toContain("transfer");
|
|
expect(projected.remediation.action).not.toMatch(/available host memory/u);
|
|
const maximum = projectAnalysisError(
|
|
new AnalysisResourceConstraintError(
|
|
"analyze_javascript_application",
|
|
"memory",
|
|
"heap",
|
|
null,
|
|
{
|
|
remediationAction: javaScriptWorkerHeapRemediation(
|
|
JAVASCRIPT_WORKER_HEAP_MIB_MAXIMUM,
|
|
),
|
|
},
|
|
),
|
|
);
|
|
expect(maximum.remediation.action).toContain("Do not increase it further");
|
|
});
|
|
|
|
it("names the parent heap control for the parent result budget", () => {
|
|
expect(JAVASCRIPT_PARENT_RESULT_BUDGET_REMEDIATION).toContain(
|
|
"parent result budget",
|
|
);
|
|
expect(JAVASCRIPT_PARENT_RESULT_BUDGET_REMEDIATION).toContain("NODE_OPTIONS");
|
|
expect(JAVASCRIPT_PARENT_RESULT_BUDGET_REMEDIATION).toContain(
|
|
"--max-old-space-size=<MiB>",
|
|
);
|
|
expect(JAVASCRIPT_PARENT_RESULT_BUDGET_REMEDIATION).toContain("max_heap_mb");
|
|
expect(JAVASCRIPT_PARENT_RESULT_BUDGET_REMEDIATION).not.toContain(
|
|
"no runtime override",
|
|
);
|
|
});
|
|
|
|
it("names the parent heap control for checkpoint decode", () => {
|
|
expect(JAVASCRIPT_PARENT_CHECKPOINT_HEAP_REMEDIATION).toContain(
|
|
"parent process heap",
|
|
);
|
|
expect(JAVASCRIPT_PARENT_CHECKPOINT_HEAP_REMEDIATION).toContain(
|
|
"NODE_OPTIONS",
|
|
);
|
|
expect(JAVASCRIPT_PARENT_CHECKPOINT_HEAP_REMEDIATION).toContain(
|
|
"--max-old-space-size=<MiB>",
|
|
);
|
|
expect(JAVASCRIPT_PARENT_CHECKPOINT_HEAP_REMEDIATION).toContain(
|
|
"admit this checkpoint",
|
|
);
|
|
expect(JAVASCRIPT_PARENT_CHECKPOINT_HEAP_REMEDIATION).toContain(
|
|
"Increasing max_heap_mb does not raise it",
|
|
);
|
|
});
|
|
|
|
it("attributes V8 heap exhaustion to max_heap_mb only when that flag is enforced", () => {
|
|
const node = javaScriptWorkerHeapExhaustionRecovery({
|
|
heapFlagsEnforced: true,
|
|
heapMb: 1024,
|
|
path: "app.js",
|
|
phase: "static",
|
|
});
|
|
expect(node.reason).toContain(
|
|
"exhausted its 1024 MiB V8 old-space heap (max_heap_mb)",
|
|
);
|
|
expect(node.remediationAction).toContain("max_heap_mb");
|
|
expect(node.remediationAction).toContain("--max-heap-mb");
|
|
const bun = javaScriptWorkerHeapExhaustionRecovery({
|
|
heapFlagsEnforced: false,
|
|
heapMb: 1024,
|
|
path: "app.js",
|
|
phase: "static",
|
|
});
|
|
expect(bun.reason).toContain("Bun ignores the V8 old-space flag");
|
|
expect(bun.reason).not.toContain(
|
|
"exhausted its 1024 MiB V8 old-space heap (max_heap_mb)",
|
|
);
|
|
expect(bun.remediationAction).toContain("Bun ignores V8 heap flags");
|
|
expect(bun.remediationAction).not.toContain("Increase max_heap_mb");
|
|
});
|
|
|
|
it("names the four semantic arguments on ceiling coverage", () => {
|
|
const both = { total: true, perFile: true };
|
|
const nodes = javaScriptSemanticCeilingCoverage("nodes", 50, 30, both);
|
|
const unknowns = javaScriptSemanticCeilingCoverage("unknowns", 3, 2, both);
|
|
const perFileOnly = javaScriptSemanticCeilingCoverage("nodes", 50, 30, {
|
|
total: false,
|
|
perFile: true,
|
|
});
|
|
const totalOnly = javaScriptSemanticCeilingCoverage("unknowns", 3, 2, {
|
|
total: true,
|
|
perFile: false,
|
|
});
|
|
expect(nodes.limits).toEqual(perFileOnly.limits);
|
|
expect(nodes.limits).toEqual(
|
|
expect.arrayContaining([
|
|
{ name: "semantic_graph_node_ceiling", value: 50, unit: "items" },
|
|
{ name: "semantic_graph_file_node_ceiling", value: 30, unit: "items" },
|
|
{ name: "max_semantic_nodes", value: 50, unit: "items" },
|
|
{ name: "max_semantic_nodes_per_file", value: 30, unit: "items" },
|
|
]),
|
|
);
|
|
expect(unknowns.limits).toEqual(totalOnly.limits);
|
|
expect(unknowns.limits).toEqual(
|
|
expect.arrayContaining([
|
|
{ name: "semantic_graph_unknown_ceiling", value: 3, unit: "items" },
|
|
{ name: "semantic_graph_file_unknown_ceiling", value: 2, unit: "items" },
|
|
{ name: "max_semantic_unknowns", value: 3, unit: "items" },
|
|
{ name: "max_semantic_unknowns_per_file", value: 2, unit: "items" },
|
|
]),
|
|
);
|
|
expect(nodes.limitation).toContain("1 to");
|
|
expect(nodes.limitation).toContain(String(JAVASCRIPT_SEMANTIC_ITEM_MAXIMUM));
|
|
expect(nodes.limitation).toContain(
|
|
"CLI: --max-semantic-nodes and --max-semantic-nodes-per-file",
|
|
);
|
|
expect(nodes.limitation).toContain(
|
|
"Set max_semantic_nodes and max_semantic_nodes_per_file",
|
|
);
|
|
expect(nodes.limitation).toContain("Raising them does not raise");
|
|
expect(nodes.limitation).toContain("next analysis");
|
|
expect(unknowns.limitation).toContain(
|
|
"CLI: --max-semantic-unknowns and --max-semantic-unknowns-per-file",
|
|
);
|
|
expect(unknowns.limitation).toContain("max_semantic_nodes");
|
|
expect(perFileOnly.limitation).toContain("max_semantic_nodes_per_file is 30");
|
|
expect(perFileOnly.limitation).toContain(
|
|
"Set max_semantic_nodes_per_file on the next",
|
|
);
|
|
expect(perFileOnly.limitation).toContain(
|
|
"CLI: --max-semantic-nodes-per-file",
|
|
);
|
|
expect(perFileOnly.limitation).toContain("Raising it does not raise");
|
|
expect(perFileOnly.limitation).not.toContain("max_semantic_nodes is 50");
|
|
expect(perFileOnly.limitation).not.toContain("--max-semantic-nodes and");
|
|
expect(totalOnly.limitation).toContain("max_semantic_unknowns is 3");
|
|
expect(totalOnly.limitation).toContain(
|
|
"Set max_semantic_unknowns on the next",
|
|
);
|
|
expect(totalOnly.limitation).toContain("CLI: --max-semantic-unknowns)");
|
|
expect(totalOnly.limitation).not.toContain(
|
|
"max_semantic_unknowns_per_file is 2",
|
|
);
|
|
expect(totalOnly.limitation).not.toContain(
|
|
"--max-semantic-unknowns-per-file",
|
|
);
|
|
const capped = javaScriptSemanticCeilingCoverage(
|
|
"nodes",
|
|
JAVASCRIPT_SEMANTIC_ITEM_MAXIMUM,
|
|
30,
|
|
{ total: true, perFile: false },
|
|
);
|
|
expect(capped.limitation).toContain("Do not increase it further");
|
|
expect(capped.limitation).not.toContain("max_semantic_nodes_per_file is 30");
|
|
expect(() =>
|
|
javaScriptSemanticCeilingCoverage("nodes", 50, 30, {
|
|
total: false,
|
|
perFile: false,
|
|
}),
|
|
).toThrow("ceiling that stopped retention");
|
|
});
|