/** * Global shutdown-hook lifecycle — a library must never keep its host process * alive. init() registers process-level SIGTERM/SIGINT/beforeExit listeners * (once, globally) whose signal handles are ref'd by Node; if they outlive the * last brain, a bare script hangs forever after close() (consumer-reported on * the GA pair). The contract pinned here: * * - first init() → exactly one listener added per signal * - more inits → no additional listeners (registered once) * - close() → listeners survive while OTHER instances remain live * - last close() → listeners removed, counts back to baseline * - re-init → hooks re-register (the once-flag resets) */ import { describe, it, expect } from 'vitest' import { Brainy, NounType } from '../../src/index.js' const SIGNALS = ['SIGTERM', 'SIGINT', 'beforeExit'] as const function listenerCounts(): Record { return Object.fromEntries(SIGNALS.map((s) => [s, process.listeners(s as any).length])) } function makeBrain(): any { return new Brainy({ requireSubtype: false, storage: { type: 'memory' }, plugins: [], silent: true }) } describe('Shutdown-hook lifecycle (process must exit after the last close)', () => { it('registers each hook once on first init and removes ALL of them on the last close', async () => { const baseline = listenerCounts() const brain = makeBrain() await brain.init() for (const s of SIGNALS) { expect(process.listeners(s as any).length).toBe(baseline[s] + 1) // exactly one each } await brain.close() expect(listenerCounts()).toEqual(baseline) // gone — the loop can drain }) it('keeps the hooks while any other instance is live; only the LAST close deregisters', async () => { const baseline = listenerCounts() const first = makeBrain() const second = makeBrain() await first.init() await second.init() for (const s of SIGNALS) { expect(process.listeners(s as any).length).toBe(baseline[s] + 1) // once globally, not per instance } await first.close() for (const s of SIGNALS) { expect(process.listeners(s as any).length).toBe(baseline[s] + 1) // second is still live } await second.close() expect(listenerCounts()).toEqual(baseline) }) it('re-registers after a full close/re-init cycle (the once-flag resets)', async () => { const baseline = listenerCounts() const first = makeBrain() await first.init() await first.close() expect(listenerCounts()).toEqual(baseline) const second = makeBrain() await second.init() const id = await second.add({ data: 'still fully functional', type: NounType.Concept }) expect(id).toBeTruthy() for (const s of SIGNALS) { expect(process.listeners(s as any).length).toBe(baseline[s] + 1) } await second.close() expect(listenerCounts()).toEqual(baseline) }) })