/** * #2649 review — pipeline-level coverage for the parse-phase heap guardrails. * * The pure predicates are covered in parse-impl-heap-guard.test.ts; these * tests pin the WIRING inside runChunkedParseAndResolve: the mid-loop abort * actually rejects the parse with the remedy message (nothing en route may * swallow or remap it — the #2441 exit-0 bug class), and the preflight * projection is computed from PARSEABLE files, not total scanned files (the * miscalibration fixed on this branch). */ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { pathToFileURL } from 'node:url'; const getHeapStatisticsMock = vi.hoisted(() => vi.fn()); vi.mock('node:v8', async () => { const actual = await vi.importActual('node:v8'); const mocked = { ...actual, getHeapStatistics: getHeapStatisticsMock }; return { ...mocked, default: mocked }; }); import { createKnowledgeGraph } from '../../src/core/graph/graph.js'; import { projectParseHeapNeedBytes, runChunkedParseAndResolve, } from '../../src/core/ingestion/pipeline-phases/parse-impl.js'; import { _captureLogger } from '../../src/core/logger.js'; const MB = 1024 * 1024; let repoDir: string; let workerStubPath: string; let memoryUsageSpy: ReturnType | undefined; beforeEach(() => { delete process.env.GITNEXUS_MEMORY; repoDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gitnexus-heap-guard-pipeline-')); fs.mkdirSync(path.join(repoDir, 'src'), { recursive: true }); // The pool validates the worker script's existence up front; the abort test // never dispatches, and the preflight test parses one real file. workerStubPath = path.join(repoDir, 'fake-worker.js'); fs.writeFileSync(workerStubPath, '// worker stub for createWorkerPool'); }); afterEach(() => { memoryUsageSpy?.mockRestore(); memoryUsageSpy = undefined; delete process.env.GITNEXUS_MEMORY; fs.rmSync(repoDir, { recursive: true, force: true }); }); const writeFixture = (rel: string, content: string): { path: string; size: number } => { const full = path.join(repoDir, rel); fs.writeFileSync(full, content); return { path: rel, size: fs.statSync(full).size }; }; describe('#2649 heap guardrails wired into runChunkedParseAndResolve', () => { it('mid-loop guard rejects the parse with the actionable remedy message', async () => { const file = writeFixture('src/a.ts', 'export function a() { return 1; }\n'); // 1GB limit with 95% "in use": above the 92% abort threshold. getHeapStatisticsMock.mockReturnValue({ heap_size_limit: 1024 * MB }); memoryUsageSpy = vi.spyOn(process, 'memoryUsage').mockReturnValue({ rss: 0, heapTotal: 1024 * MB, heapUsed: 973 * MB, external: 0, arrayBuffers: 0, }); const graph = createKnowledgeGraph(); await expect( runChunkedParseAndResolve(graph, [file], [file.path], 1, repoDir, Date.now(), () => {}, { workerUrlForTest: pathToFileURL(workerStubPath), workerPoolSize: 1, }), ).rejects.toThrow(/Analyze stopped before running out of memory/); }); it('preflight warn projects from PARSEABLE files only and names the parseable count', async () => { // Mock the heap limit relative to what ONE parseable file actually projects, // so this stays a test of the WARN BEHAVIOUR rather than of the projection // constant. Hard-coding 150_000 tied it to PROJECTED_HEAP_BYTES_PER_NODE = // 2250; recalibrating that constant for streamed emit (#2680) dropped the // projection to 0.80 of the limit and the warn silently stopped firing. // Deriving the limit keeps the ratio at 0.90 — above the 0.85 threshold — // whatever the constant becomes. process.env.GITNEXUS_MEMORY = 'off'; getHeapStatisticsMock.mockReturnValue({ heap_size_limit: Math.floor(projectParseHeapNeedBytes(1) / 0.9), }); const parseable = writeFixture('src/b.ts', 'export function b() { return 2; }\n'); const unparseable = writeFixture('src/data.zzz9', 'not source code\n'); const cap = _captureLogger(); const graph = createKnowledgeGraph(); try { await runChunkedParseAndResolve( graph, [parseable, unparseable], [parseable.path, unparseable.path], 2, repoDir, Date.now(), () => {}, { workerUrlForTest: pathToFileURL( path.resolve( __dirname, '..', '..', 'dist', 'core', 'ingestion', 'workers', 'parse-worker.js', ), ), workerPoolSize: 1, }, ); } finally { cap.restore(); } const warn = cap.records().find((r) => r.msg.includes('Large repository')); // "analyzing 1 files" — the parseable count, not the 2 scanned files. expect({ fired: warn !== undefined, parseableBasis: warn?.msg.includes('analyzing 1 files') ?? false, }).toEqual({ fired: true, parseableBasis: true }); }); });