import { DyNTS_global_settings } from '../../_collections/global-settings.const'; import { DyNTS_GlobalService } from './global.service'; import { DyNTS_MemoryGuard } from './memory-guard.service'; /** * FR-193 — a bedrock heap-watchdog spec-je. A `poll()`-t KÖZVETLENÜL hívjuk (nem * az intervallumra várunk), mockolt heap-plafonnal + `process.memoryUsage`-dzsel, * így determinisztikusan teszteljük a hiszterézis-állapotgépet + a tartós rögzítést. */ const MB: number = 1024 * 1024; /** * A privát `getHeapLimitBytes` + `process.memoryUsage` mockolása adott heap-%-hoz. * Visszaadja a memoryUsage-spy-t, hogy a hívó később átállíthassa (recovery-teszt). * `process as any`: a `MemoryUsageFn` típus egy `.rss()` submetódust is hordoz, amit * egy egyszerű return-value mock nem elégít ki — a cast kikerüli ezt a teszt-zajt. */ const mockHeap = (guard: DyNTS_MemoryGuard, limitMb: number, usedMb: number): jasmine.Spy => { spyOn(guard as any, 'getHeapLimitBytes').and.returnValue(limitMb * MB); return spyOn(process as any, 'memoryUsage').and.returnValue({ heapUsed: usedMb * MB, heapTotal: usedMb * MB, rss: (usedMb + 100) * MB, external: 10 * MB, arrayBuffers: 0, } as NodeJS.MemoryUsage); }; /** Egy poll-ciklus kézi kiváltása. */ const poll = (guard: DyNTS_MemoryGuard): void => { (guard as any).poll(); }; describe('| DyNTS_MemoryGuard (FR-193)', (): void => { let guard: DyNTS_MemoryGuard; let sinkSpy: jasmine.Spy; let originalSink: typeof DyNTS_GlobalService.globalErrorHandler; let originalSettings: typeof DyNTS_global_settings.memoryGuard; beforeEach((): void => { guard = DyNTS_MemoryGuard.getInstance(); guard._teardownForTesting(); originalSettings = DyNTS_global_settings.memoryGuard; DyNTS_global_settings.memoryGuard = { enabled: true, pollIntervalMs: 999999, // ne tüzeljen az intervallum a teszt alatt heapWarningThreshold: 85, heapCriticalThreshold: 95, recoveryMargin: 10, maxHistoryCount: 100, gcWarningFraction: 0.40, gcCriticalFraction: 0.60, // A tesztek NE regisztráljanak process-szintű crash-handlert / ne lépjenek ki (a jasmine-harness védelme). installCrashHandlers: false, exitOnSustainedCritical: false, }; originalSink = DyNTS_GlobalService.globalErrorHandler; sinkSpy = jasmine.createSpy('globalErrorHandler'); (DyNTS_GlobalService as any).globalErrorHandler = sinkSpy; }); afterEach((): void => { guard._teardownForTesting(); (DyNTS_GlobalService as any).globalErrorHandler = originalSink; DyNTS_global_settings.memoryGuard = originalSettings; }); it('| install() idempotens — második hívás no-op', (): void => { guard.install(); expect(guard.isInstalled()).toBe(true); guard.install(); // nem dob, nem indít új timert expect(guard.isInstalled()).toBe(true); }); it('| install() SOHA nem dob, akkor sem ha a settings hiányos', (): void => { DyNTS_global_settings.memoryGuard = undefined as any; expect((): void => guard.install()).not.toThrow(); expect(guard.isInstalled()).toBe(true); }); it('| warning küszöb alatt NINCS esemény', (): void => { guard.install(); mockHeap(guard, 1000, 800); // 80% < 85 poll(guard); expect(guard.getHistory().length).toBe(0); expect(sinkSpy).not.toHaveBeenCalled(); }); it('| warning küszöb átlépése → 1 warning esemény + tartós error-sink (level warning)', (): void => { guard.install(); mockHeap(guard, 1000, 880); // 88% ∈ [85, 95) poll(guard); const hist: ReturnType = guard.getHistory(); expect(hist.length).toBe(1); expect(hist[0].level).toBe('warning'); expect(hist[0].heapPct).toBe(88); expect(sinkSpy).toHaveBeenCalledTimes(1); expect(guard.getStatus().state).toBe('warning'); }); it('| warning-sávban maradva NINCS re-emit (hiszterézis)', (): void => { guard.install(); mockHeap(guard, 1000, 880); // 88% → warning poll(guard); poll(guard); // még mindig 88% (ugyanaz a mock) → nincs új esemény expect(guard.getHistory().length).toBe(1); }); it('| critical küszöb átlépése → critical esemény + onCritical hook', (): void => { const onCritical: jasmine.Spy = jasmine.createSpy('onCritical'); guard.install({ onCritical: onCritical }); mockHeap(guard, 1000, 970); // 97% ≥ 95 poll(guard); const hist: ReturnType = guard.getHistory(); expect(hist[hist.length - 1].level).toBe('critical'); expect(onCritical).toHaveBeenCalledTimes(1); expect(guard.getStatus().state).toBe('critical'); }); it('| helyreállás: a warning-margó alá esve → recovered esemény (sink NÉLKÜL)', (): void => { guard.install(); const muSpy: jasmine.Spy = mockHeap(guard, 1000, 880); // 88% → warning poll(guard); sinkSpy.calls.reset(); muSpy.and.returnValue({ heapUsed: 700 * MB, heapTotal: 700 * MB, rss: 800 * MB, external: 10 * MB, arrayBuffers: 0, } as NodeJS.MemoryUsage); // 70% ≤ 85 - 10 poll(guard); const hist: ReturnType = guard.getHistory(); expect(hist[hist.length - 1].level).toBe('recovered'); expect(guard.getStatus().state).toBe('normal'); expect(sinkSpy).not.toHaveBeenCalled(); // recovered NEM ír az error-sinkbe }); it('| history ring-buffer a maxHistoryCount-ra van kapva', (): void => { DyNTS_global_settings.memoryGuard!.maxHistoryCount = 3; guard.install(); spyOn(guard as any, 'getHeapLimitBytes').and.returnValue(1000 * MB); const high: NodeJS.MemoryUsage = { heapUsed: 880 * MB, heapTotal: 880 * MB, rss: 980 * MB, external: 0, arrayBuffers: 0 } as NodeJS.MemoryUsage; const low: NodeJS.MemoryUsage = { heapUsed: 700 * MB, heapTotal: 700 * MB, rss: 800 * MB, external: 0, arrayBuffers: 0 } as NodeJS.MemoryUsage; let tick: number = 0; // warning ↔ recovered oszcilláció: páros tick=high (warning), páratlan=low (recovered) spyOn(process as any, 'memoryUsage').and.callFake((): NodeJS.MemoryUsage => (tick++ % 2 === 0 ? high : low)); for (let k: number = 0; k < 8; k++) { poll(guard); } // 8 állapot-váltás, csak 3 marad expect(guard.getHistory().length).toBe(3); }); it('| poll() SOHA nem dob (heap limit 0 → no-op)', (): void => { guard.install(); spyOn(guard as any, 'getHeapLimitBytes').and.returnValue(0); expect((): void => poll(guard)).not.toThrow(); expect(guard.getHistory().length).toBe(0); }); it('| getStatus a csúcsokat is követi', (): void => { guard.install(); mockHeap(guard, 1000, 920); // 92% poll(guard); expect(guard.getStatus().peakHeapPct).toBe(92); expect(guard.getStatus().peakRssMb).toBeGreaterThan(0); }); // --- GC-thrash precursor (a %-küszöb ALATT is OOM-jel) --- it('| GC-thrash kritikus, bár a heap-% csak 70% (a %-küszöb ALATT) — trigger=gc', (): void => { guard.install(); mockHeap(guard, 1000, 700); // 70% < warn 85 spyOn(guard as any, 'recentGcFraction').and.returnValue(0.65); // GC 65% ≥ crit 60% poll(guard); const hist: ReturnType = guard.getHistory(); expect(hist[hist.length - 1].level).toBe('critical'); expect(hist[hist.length - 1].trigger).toBe('gc'); expect(sinkSpy).toHaveBeenCalledTimes(1); }); it('| közepes GC (40–60%) alacsony heap-nél → warning (trigger=gc)', (): void => { guard.install(); mockHeap(guard, 1000, 700); // 70% spyOn(guard as any, 'recentGcFraction').and.returnValue(0.45); poll(guard); expect(guard.getHistory()[0].level).toBe('warning'); expect(guard.getHistory()[0].trigger).toBe('gc'); }); // --- pressure() / shouldShedLoad() — a fogyasztói load-shed gate --- it('| pressure() a heap-% + GC közül a rosszabb szintet adja; shouldShedLoad critical-nál true', (): void => { guard.install(); mockHeap(guard, 1000, 970); // 97% ≥ crit spyOn(guard as any, 'recentGcFraction').and.returnValue(0.0); const p = guard.pressure(); expect(p.level).toBe('critical'); expect(p.heapPct).toBe(97); expect(guard.shouldShedLoad()).toBe(true); }); it('| shouldShedLoad false, ha ok-szint (alacsony heap + alacsony GC)', (): void => { guard.install(); mockHeap(guard, 1000, 500); // 50% spyOn(guard as any, 'recentGcFraction').and.returnValue(0.10); expect(guard.pressure().level).toBe('ok'); expect(guard.shouldShedLoad()).toBe(false); }); // --- graceful-exit: tartós critical → exit (a wrapper újraindít) --- it('| exitOnSustainedCritical: N egymás utáni critical poll → gracefulExit (process.exit)', (): void => { DyNTS_global_settings.memoryGuard!.exitOnSustainedCritical = true; DyNTS_global_settings.memoryGuard!.sustainedCriticalPolls = 3; DyNTS_global_settings.memoryGuard!.bootGraceMs = 0; // nincs grace → azonnal élesedik const exitSpy: jasmine.Spy = spyOn(guard as any, 'gracefulExit').and.stub(); guard.install(); mockHeap(guard, 1000, 970); // 97% critical spyOn(guard as any, 'recentGcFraction').and.returnValue(0.0); poll(guard); poll(guard); expect(exitSpy).not.toHaveBeenCalled(); // 2 critical < 3 poll(guard); expect(exitSpy).toHaveBeenCalledTimes(1); // a 3. critical → exit }); it('| BOOT-GRACE: a grace alatt a sustained-critical NEM lép ki; a grace után IGEN', (): void => { DyNTS_global_settings.memoryGuard!.exitOnSustainedCritical = true; DyNTS_global_settings.memoryGuard!.sustainedCriticalPolls = 3; DyNTS_global_settings.memoryGuard!.bootGraceMs = 60000; // 60s grace const exitSpy: jasmine.Spy = spyOn(guard as any, 'gracefulExit').and.stub(); guard.install(); mockHeap(guard, 1000, 970); // 97% critical spyOn(guard as any, 'recentGcFraction').and.returnValue(0.0); // A grace ALATT (installedAt épp most): hiába N+ critical poll, NINCS exit. poll(guard); poll(guard); poll(guard); poll(guard); expect(exitSpy).not.toHaveBeenCalled(); // A grace UTÁN (installedAt 61s-cel ezelőttre állítva): a következő critical poll már kilép. (guard as any).installedAt = Date.now() - 61000; poll(guard); expect(exitSpy).toHaveBeenCalledTimes(1); }); });