import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; // Pin physical RAM to 32GB so the remedy branch (auto cap = 24GB) resolves // deterministically regardless of the host machine. vi.mock('os', async () => { const actual = await vi.importActual('os'); const mocked = { ...actual, totalmem: () => 32 * 1024 * 1024 * 1024 }; return { ...mocked, default: mocked }; }); import { projectParseHeapNeedBytes, shouldAbortForHeapPressure, } from '../../src/core/ingestion/pipeline-phases/parse-impl.js'; import { heapPressureRemedy } from '../../src/core/ingestion/utils/effective-ram.js'; const GB = 1024 * 1024 * 1024; const setConstrainedMemory = (value: number): (() => void) => { const desc = Object.getOwnPropertyDescriptor(process, 'constrainedMemory'); Object.defineProperty(process, 'constrainedMemory', { configurable: true, value: () => value }); return () => { if (desc) Object.defineProperty(process, 'constrainedMemory', desc); else delete (process as { constrainedMemory?: unknown }).constrainedMemory; }; }; describe('#2649 parse-phase heap guardrails', () => { let initialGuard: string | undefined; let initialSource: string | undefined; let restoreConstrained: (() => void) | undefined; beforeEach(() => { initialGuard = process.env.GITNEXUS_MEMORY; initialSource = process.env.GITNEXUS_HEAP_LIMIT_SOURCE; delete process.env.GITNEXUS_MEMORY; delete process.env.GITNEXUS_HEAP_LIMIT_SOURCE; // Unconstrained by default so the mocked 32GB totalmem governs. restoreConstrained = setConstrainedMemory(0); }); afterEach(() => { if (initialGuard === undefined) delete process.env.GITNEXUS_MEMORY; else process.env.GITNEXUS_MEMORY = initialGuard; if (initialSource === undefined) delete process.env.GITNEXUS_HEAP_LIMIT_SOURCE; else process.env.GITNEXUS_HEAP_LIMIT_SOURCE = initialSource; restoreConstrained?.(); restoreConstrained = undefined; }); it('projects kernel-scale repos far past the 4GB default heap and small repos well under it', () => { // 94,773 files x 55 nodes x 2250 bytes ≈ 11.7GB (the measured #2649 case); // 2,000 files ≈ 236MB. expect({ kernelExceeds4Gb: projectParseHeapNeedBytes(94773) > 4 * GB, smallRepoUnder1Gb: projectParseHeapNeedBytes(2000) < 1 * GB, }).toEqual({ kernelExceeds4Gb: true, smallRepoUnder1Gb: true }); }); it('aborts above 92% of the heap limit and not below it', () => { const limit = 4 * GB; expect([0.91, 0.93].map((f) => shouldAbortForHeapPressure(limit * f, limit))).toEqual([ false, true, ]); }); it('GITNEXUS_MEMORY=0 disables the abort entirely', () => { process.env.GITNEXUS_MEMORY = 'off'; const limit = 4 * GB; expect(shouldAbortForHeapPressure(limit * 0.99, limit)).toBe(false); }); it('points at the NODE_OPTIONS pin when the machine has more memory to give', () => { // 4GB limit on a 32GB machine (auto cap 24GB): the pin is the problem. expect(heapPressureRemedy(4 * GB)).toContain('re-run without the --max-old-space-size'); }); it('points at scope or hardware when the machine is the ceiling', () => { // 23GB limit on a 32GB machine (~auto cap): nothing more to unlock locally. expect(heapPressureRemedy(23 * GB)).toContain('.gitnexusignore'); }); it('remedy respects a real cgroup limit: a memory-limited container is never told to "drop the pin" (#2649 review)', () => { // 8GB cgroup limit on the mocked 32GB host, heap already sized to the // container (~6.5GB): raw totalmem would claim "more memory available"; // the container is actually at its ceiling. restoreConstrained?.(); restoreConstrained = setConstrainedMemory(8 * GB); expect(heapPressureRemedy(6.5 * GB)).toContain('.gitnexusignore'); }); it('a --memory-budget heap is never told to drop a --max-old-space-size pin (#3137)', () => { // 4GB budget on a 32GB machine: the budget, not a pin, set the limit. process.env.GITNEXUS_HEAP_LIMIT_SOURCE = 'budget'; const remedy = heapPressureRemedy(4 * GB); expect({ mentionsPin: remedy.includes('--max-old-space-size'), mentionsBudgetFlag: remedy.includes('--memory-budget'), }).toEqual({ mentionsPin: false, mentionsBudgetFlag: true }); }); it('GITNEXUS_MEMORY=off without a pin is not told to drop a --max-old-space-size pin', () => { process.env.GITNEXUS_MEMORY = 'off'; const remedy = heapPressureRemedy(4 * GB); expect({ mentionsPin: remedy.includes('--max-old-space-size'), mentionsMemoryOff: remedy.includes('GITNEXUS_MEMORY=off'), }).toEqual({ mentionsPin: false, mentionsMemoryOff: true }); }); it('measures pressure against the auto-sized cap, not a flat 0.75 × RAM', () => { // 7500MB cgroup: the auto cap is 6000MB (0.80 × RAM), so a 5300MB pin is // below 90% of it and still gets the drop-the-pin advice. restoreConstrained?.(); restoreConstrained = setConstrainedMemory(7500 * 1024 * 1024); expect(heapPressureRemedy(5300 * 1024 * 1024)).toContain('--max-old-space-size'); }); it('a --memory-budget heap at the machine ceiling gets the scope-or-hardware advice', () => { process.env.GITNEXUS_HEAP_LIMIT_SOURCE = 'budget'; const budgetRemedy = heapPressureRemedy(23 * GB); delete process.env.GITNEXUS_HEAP_LIMIT_SOURCE; expect(budgetRemedy).toBe(heapPressureRemedy(23 * GB)); }); });