import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test'; import { Shutdown } from '../src/shutdown'; describe('Shutdown Signal Handlers', () => { let shutdown: Shutdown; let processOnSpy: any; let processExitSpy: any; const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform'); const originalOn = process.on; const originalExit = process.exit; beforeEach(() => { // Reset singleton instance (Shutdown as any).instance = null; // Clean up global flag delete (global as any).__SHUTDOWN_SIGNAL_RECEIVED__; // Mock process.on const listeners: Record = {}; processOnSpy = mock((event: string, handler: Function) => { if (!listeners[event]) { listeners[event] = []; } listeners[event].push(handler); }); process.on = processOnSpy as any; // Mock process.exit processExitSpy = mock((code?: number) => { // Just record the call, don't throw return; }); process.exit = processExitSpy as any; // Store listeners for manual triggering (global as any).__testListeners = listeners; }); afterEach(() => { // Restore original methods process.on = originalOn; process.exit = originalExit; if (originalPlatform) { Object.defineProperty(process, 'platform', originalPlatform); } // Clean up (Shutdown as any).instance = null; delete (global as any).__testListeners; }); describe('Signal Handler Registration', () => { it('should register Unix signal handlers on non-Windows', () => { Object.defineProperty(process, 'platform', { value: 'linux', configurable: true, }); shutdown = new Shutdown({ autoRegister: true }); // Check that Unix signals were registered expect(processOnSpy).toHaveBeenCalledWith('SIGTERM', expect.any(Function)); expect(processOnSpy).toHaveBeenCalledWith('SIGINT', expect.any(Function)); expect(processOnSpy).toHaveBeenCalledWith('SIGUSR2', expect.any(Function)); expect(processOnSpy).toHaveBeenCalledWith('uncaughtException', expect.any(Function)); expect(processOnSpy).toHaveBeenCalledWith('unhandledRejection', expect.any(Function)); }); it('should register Windows signal handlers on Windows', () => { Object.defineProperty(process, 'platform', { value: 'win32', configurable: true, }); shutdown = new Shutdown({ autoRegister: true }); // Check that Windows signals were registered expect(processOnSpy).toHaveBeenCalledWith('SIGTERM', expect.any(Function)); expect(processOnSpy).toHaveBeenCalledWith('SIGINT', expect.any(Function)); expect(processOnSpy).not.toHaveBeenCalledWith('SIGUSR2', expect.any(Function)); expect(processOnSpy).toHaveBeenCalledWith('uncaughtException', expect.any(Function)); expect(processOnSpy).toHaveBeenCalledWith('unhandledRejection', expect.any(Function)); }); it('should not register handlers when autoRegister is false', () => { shutdown = new Shutdown({ autoRegister: false }); expect(processOnSpy).not.toHaveBeenCalled(); }); it('should not register handlers twice', () => { shutdown = new Shutdown({ autoRegister: true }); const callCount = processOnSpy.mock.calls.length; // Try to setup handlers again (internally) shutdown['setupSignalHandlers'](); // Should not register additional handlers expect(processOnSpy.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]; // Trigger SIGTERM (this starts async shutdown) sigtermHandler(); // Verify flags are set immediately expect(shutdown.isShutdownSignalReceived()).toBe(true); expect((global as any).__SHUTDOWN_SIGNAL_RECEIVED__).toBe(true); // Wait a bit for async shutdown to complete await new Promise(resolve => setTimeout(resolve, 10)); // Now process.exit should have been called 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]; // Trigger SIGINT (this starts async shutdown) sigintHandler(); // Verify flags are set immediately expect(shutdown.isShutdownSignalReceived()).toBe(true); // Wait a bit for async shutdown to complete await new Promise(resolve => setTimeout(resolve, 10)); // Now process.exit should have been called expect(processExitSpy).toHaveBeenCalledWith(0); }); it('should handle uncaughtException', async () => { shutdown = new Shutdown({ autoRegister: true }); const listeners = (global as any).__testListeners; const exceptionHandler = listeners['uncaughtException'][0]; // Trigger uncaughtException (this starts async shutdown with exit code 1) exceptionHandler(new Error('Uncaught error')); // Wait a bit for async shutdown to complete await new Promise(resolve => setTimeout(resolve, 10)); // Should exit with code 1 for uncaught exceptions expect(processExitSpy).toHaveBeenCalledWith(1); }); it('should handle unhandledRejection', async () => { shutdown = new Shutdown({ autoRegister: true }); const listeners = (global as any).__testListeners; const rejectionHandler = listeners['unhandledRejection'][0]; // Trigger unhandledRejection (this starts async shutdown with exit code 1) rejectionHandler(new Error('Unhandled rejection')); // Wait a bit for async shutdown to complete await new Promise(resolve => setTimeout(resolve, 10)); // Should exit with code 1 for unhandled rejections expect(processExitSpy).toHaveBeenCalledWith(1); }); it('should not process signal if already shutting down', async () => { shutdown = new Shutdown({ autoRegister: true }); // Start shutdown shutdown['isShuttingDown'] = true; const listeners = (global as any).__testListeners; const sigtermHandler = listeners['SIGTERM'][0]; // Mock shutdownAndExit to track calls const shutdownAndExitSpy = mock(() => Promise.resolve()); shutdown.shutdownAndExit = shutdownAndExitSpy as any; // Trigger SIGTERM sigtermHandler(); // Should not call shutdownAndExit since already shutting down expect(shutdownAndExitSpy).not.toHaveBeenCalled(); }); it('should handle shutdown failure in signal handler', async () => { shutdown = new Shutdown({ autoRegister: true }); // Mock shutdownAndExit to reject shutdown.shutdownAndExit = mock(async () => { throw new Error('Shutdown failed'); }) as any; const listeners = (global as any).__testListeners; const sigtermHandler = listeners['SIGTERM'][0]; // Trigger SIGTERM - should fall back to process.exit(1) sigtermHandler(); // Wait a bit for async shutdown to fail and fallback to occur await new Promise(resolve => setTimeout(resolve, 10)); expect(processExitSpy).toHaveBeenCalledWith(1); }); }); describe('Global Flag Behavior', () => { it('should set global shutdown flag on signal', async () => { delete (global as any).__SHUTDOWN_SIGNAL_RECEIVED__; shutdown = new Shutdown({ autoRegister: true }); const listeners = (global as any).__testListeners; const sigtermHandler = listeners['SIGTERM'][0]; // Trigger signal (this sets the flag immediately) sigtermHandler(); expect((global as any).__SHUTDOWN_SIGNAL_RECEIVED__).toBe(true); // Wait for async shutdown to complete to avoid hanging promises await new Promise(resolve => setTimeout(resolve, 10)); }); it('should check global flag in isShutdownSignalReceived', () => { shutdown = new Shutdown({ autoRegister: false }); expect(shutdown.isShutdownSignalReceived()).toBe(false); // Set global flag (global as any).__SHUTDOWN_SIGNAL_RECEIVED__ = true; // Even without instance flag, should return true expect(shutdown.isShutdownSignalReceived()).toBe(true); // Clean up delete (global as any).__SHUTDOWN_SIGNAL_RECEIVED__; }); }); });