import { createHash } from "node:crypto"; import { readFile, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { expect } from "vitest"; import { z } from "zod"; import { parseEvidence } from "../../../src/domain/evidence.js"; import { javascriptApplicationAnalysisResultSchema } from "../../../src/domain/javascript/javascriptApplicationAnalysis.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; import { cliTest } from "../../support/cli/cliFixture.js"; cliTest.skipIf(process.versions.bun !== undefined).for([ { laterSources: 1, heapMb: 640, methodsCount: 400, retainGraph: true }, { laterSources: 10, heapMb: 640, methodsCount: 400, retainGraph: true }, // Even an otherwise empty parent cannot admit this graph after reserving // transient decoding and the next checkpoint, independent of GC timing. { laterSources: 1, heapMb: 448, methodsCount: 600, retainGraph: false }, ])( "preserves evidence with $laterSources later checkpoints and a $heapMb MiB parent heap", { timeout: 90_000 }, async ({ laterSources, heapMb, methodsCount, retainGraph }, { cli }) => { const root = await createTestTempDirectory("rea-js-checkpoint-reserve-"); const source = Array.from( { length: methodsCount }, (_, index) => `export function method_${String(index)}(input) { const value = input + ${String(index)}; const pair = { value }; return pair.value; }`, ).join("\n"); const files: readonly (readonly [string, string])[] = [ ["a-large.mjs", source], ...Array.from( { length: laterSources }, (_, index) => [ `z-tiny-${String(index)}.mjs`, `export const tiny${String(index)} = 1;\n`, ] as const, ), ]; for (const [name, text] of files) await writeFile(join(root, name), text); const response = await cli.run({ arguments: ["analyze", root, "--format", "json"], environment: { NODE_OPTIONS: `--max-old-space-size=${String(heapMb)} --max-semi-space-size=8`, }, timeoutMs: 60_000, }); expect(response.exitCode).toBe(0); const result = javascriptApplicationAnalysisResultSchema.parse( parseEvidence(response.json).normalized_result, ); expect(result.statistics.parsed_javascript_files).toBe(laterSources + 1); expect(result.statistics.parse_failures).toBe(0); const failures = result.graph.nodes.flatMap(({ observations }) => observations.flatMap(({ properties }) => properties.analysis_failure === undefined ? [] : [properties.analysis_failure], ), ); const methods = result.semantic_graph.nodes.filter( ({ kind, identity }) => kind === "function" && identity.module_path === "a-large.mjs", ); if (retainGraph) expect(methods.map(({ label }) => label).sort()).toEqual( Array.from( { length: methodsCount }, (_, index) => `method_${String(index)}`, ).sort(), ); else { expect(methods).toHaveLength(0); expect([ result.graph.coverage.status, result.semantic_graph.coverage.status, ]).toContain("partial"); } expect( result.semantic_graph.nodes.some( ({ identity, label }) => identity.module_path === `z-tiny-${String(laterSources - 1)}.mjs` && label === `tiny${String(laterSources - 1)}`, ), ).toBe(true); for (const [name, text] of files) { const digest = createHash("sha256").update(text).digest("hex"); const node = result.graph.nodes.find( ({ kind, identity }) => kind === "javascript-asset" && identity.strategy === "content-digest" && identity.sha256 === digest, ); expect(node?.observations).toEqual( expect.arrayContaining([ expect.objectContaining({ properties: expect.objectContaining({ path: name, parse_status: "complete", }), }), ]), ); if (retainGraph) expect(failures).toEqual([]); expect(await readFile(join(root, name), "utf8")).toBe(text); if (!retainGraph && name === "a-large.mjs") { const failure = z .object({ code: z.literal("resource_constraint"), remediation: z.object({ action: z.string() }), details: z.object({ reported_limits: z.object({ parent_available_result_budget_bytes: z.number(), module_retention_estimate_bytes: z.number(), graph_retention_estimate_bytes: z.number(), }), }), }) .parse(failures[0]); const limits = failure.details.reported_limits; expect( limits.module_retention_estimate_bytes + limits.graph_retention_estimate_bytes, ).toBeGreaterThan(limits.parent_available_result_budget_bytes); expect(failure.remediation.action).toContain("parent result budget"); expect(failure.remediation.action).toContain( "retained static and module facts", ); } } expect(response.stderr).not.toMatch(/FATAL ERROR:.*heap/iu); }, );