import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test'; import { Shutdown } from '../src/shutdown'; describe('Shutdown Signal Handlers', () => { let shutdown: Shutdown; let processOnceSpy: any; let processExitSpy: any; const originalOnce = process.once; const originalExit = process.exit; beforeEach(() => { // Reset singleton instance (Shutdown as any).instance = null; // Mock process.once to capture signal handlers const listeners: Record = {}; processOnceSpy = mock((event: string, handler: Function) => { if (!listeners[event]) { listeners[event] = []; } listeners[event].push(handler); return process; }); process.once = processOnceSpy as any; // Mock process.exit processExitSpy = mock((code?: number) => { // Just record the call, don't actually exit return undefined as never; }); process.exit = processExitSpy as any; // Store listeners for manual triggering (global as any).__testListeners = listeners; }); afterEach(() => { // Restore original methods process.once = originalOnce; process.exit = originalExit; // Clean up (Shutdown as any).instance = null; delete (global as any).__testListeners; }); describe('Signal Handler Registration', () => { it('should register signal handlers on initialization with autoRegister', () => { shutdown = new Shutdown({ autoRegister: true }); // Check that signals were registered expect(processOnceSpy).toHaveBeenCalledWith('SIGTERM', expect.any(Function)); expect(processOnceSpy).toHaveBeenCalledWith('SIGINT', expect.any(Function)); }); it('should not register handlers when autoRegister is false', () => { shutdown = new Shutdown({ autoRegister: false }); expect(processOnceSpy).not.toHaveBeenCalled(); }); it('should not register handlers twice', () => { shutdown = new Shutdown({ autoRegister: true }); const callCount = processOnceSpy.mock.calls.length; // Try to setup handlers again (internally) shutdown['setupSignalHandlers'](); // Should not register additional handlers expect(processOnceSpy.mock.calls.length).toBe(callCount); }); }); describe('Signal Handler Behavior', () => { it('should handle SIGTERM signal', async () => { shutdown = new Shutdown({ autoRegister: true }); const callback = mock(async () => {}); shutdown.onShutdown(callback); const listeners = (global as any).__testListeners; const sigtermHandler = listeners['SIGTERM']?.[0]; expect(sigtermHandler).toBeDefined(); // Mock the shutdown method to track it was called const shutdownSpy = mock(shutdown.shutdown.bind(shutdown)); shutdown.shutdown = shutdownSpy; // Trigger SIGTERM await sigtermHandler(); // Verify shutdown was called expect(shutdownSpy).toHaveBeenCalled(); expect(processExitSpy).toHaveBeenCalledWith(0); }); it('should handle SIGINT signal', async () => { shutdown = new Shutdown({ autoRegister: true }); const callback = mock(async () => {}); shutdown.onShutdown(callback); const listeners = (global as any).__testListeners; const sigintHandler = listeners['SIGINT']?.[0]; expect(sigintHandler).toBeDefined(); // Mock the shutdown method const shutdownSpy = mock(shutdown.shutdown.bind(shutdown)); shutdown.shutdown = shutdownSpy; // Trigger SIGINT await sigintHandler(); // Verify shutdown was called expect(shutdownSpy).toHaveBeenCalled(); expect(processExitSpy).toHaveBeenCalledWith(0); }); it('should exit with code 1 on shutdown error', async () => { shutdown = new Shutdown({ autoRegister: true }); // Make shutdown throw an error shutdown.shutdown = mock(async () => { throw new Error('Shutdown failed'); }); const listeners = (global as any).__testListeners; const sigtermHandler = listeners['SIGTERM']?.[0]; // Trigger SIGTERM await sigtermHandler(); // Should exit with code 1 on error expect(processExitSpy).toHaveBeenCalledWith(1); }); it('should not process signal if already shutting down', async () => { shutdown = new Shutdown({ autoRegister: true }); // Set shutting down flag shutdown['isShuttingDown'] = true; const listeners = (global as any).__testListeners; const sigtermHandler = listeners['SIGTERM']?.[0]; // Mock shutdown to ensure it's not called const shutdownSpy = mock(shutdown.shutdown.bind(shutdown)); shutdown.shutdown = shutdownSpy; // Trigger SIGTERM await sigtermHandler(); // Should not call shutdown or exit expect(shutdownSpy).not.toHaveBeenCalled(); expect(processExitSpy).not.toHaveBeenCalled(); }); }); describe('Singleton Behavior with Signals', () => { it('should use singleton instance for signal handling', () => { const instance1 = Shutdown.getInstance(); const instance2 = Shutdown.getInstance(); expect(instance1).toBe(instance2); // Only one set of signal handlers should be registered expect(processOnceSpy).toHaveBeenCalledTimes(2); // SIGTERM and SIGINT }); it('should handle callbacks registered before signal', async () => { shutdown = new Shutdown({ autoRegister: true }); const callback1 = mock(async () => {}); const callback2 = mock(async () => {}); shutdown.onShutdown(callback1, 10); shutdown.onShutdown(callback2, 20); const listeners = (global as any).__testListeners; const sigtermHandler = listeners['SIGTERM']?.[0]; // Replace shutdown method to test callback execution const originalShutdown = shutdown.shutdown.bind(shutdown); let shutdownCalled = false; shutdown.shutdown = mock(async () => { shutdownCalled = true; await originalShutdown(); }); // Trigger signal await sigtermHandler(); expect(shutdownCalled).toBe(true); expect(processExitSpy).toHaveBeenCalledWith(0); }); }); describe('Edge Cases', () => { it('should handle missing signal handler gracefully', () => { shutdown = new Shutdown({ autoRegister: true }); const listeners = (global as any).__testListeners; // Verify handlers exist expect(listeners['SIGTERM']).toBeDefined(); expect(listeners['SIGINT']).toBeDefined(); expect(listeners['SIGTERM'].length).toBeGreaterThan(0); expect(listeners['SIGINT'].length).toBeGreaterThan(0); }); it('should work with default options', () => { // Create with defaults shutdown = new Shutdown(); // Should auto-register by default expect(processOnceSpy).toHaveBeenCalled(); }); }); });