import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test'; import { getShutdownCallbackCount, initiateShutdown, isShutdownSignalReceived, isShuttingDown, onShutdown, onShutdownHigh, onShutdownLow, onShutdownMedium, resetShutdown, setShutdownTimeout, Shutdown, } from '../src'; import type { ShutdownOptions, ShutdownResult } from '../src/types'; describe('Shutdown Comprehensive Tests', () => { beforeEach(() => { // Reset before each test resetShutdown(); }); afterEach(() => { // Clean up after each test resetShutdown(); }); describe('Global Functions', () => { describe('onShutdown', () => { it('should register callback with custom priority', () => { const callback = mock(async () => {}); onShutdown(callback, 'custom-handler', 25); expect(getShutdownCallbackCount()).toBe(1); }); it('should handle callback without name', () => { const callback = mock(async () => {}); onShutdown(callback); expect(getShutdownCallbackCount()).toBe(1); }); }); describe('Priority convenience functions', () => { it('should register high priority callback', () => { const callback = mock(async () => {}); onShutdownHigh(callback, 'high-priority'); expect(getShutdownCallbackCount()).toBe(1); }); it('should register medium priority callback', () => { const callback = mock(async () => {}); onShutdownMedium(callback, 'medium-priority'); expect(getShutdownCallbackCount()).toBe(1); }); it('should register low priority callback', () => { const callback = mock(async () => {}); onShutdownLow(callback, 'low-priority'); expect(getShutdownCallbackCount()).toBe(1); }); it('should execute callbacks in priority order', async () => { const order: string[] = []; const highCallback = mock(async () => { order.push('high'); }); const mediumCallback = mock(async () => { order.push('medium'); }); const lowCallback = mock(async () => { order.push('low'); }); onShutdownLow(lowCallback, 'low'); onShutdownHigh(highCallback, 'high'); onShutdownMedium(mediumCallback, 'medium'); await initiateShutdown(); expect(order).toEqual(['high', 'medium', 'low']); }); }); describe('setShutdownTimeout', () => { it('should set custom timeout', () => { setShutdownTimeout(10000); // Timeout is set internally, we can't directly verify it // but we can test it works by using a long-running callback expect(() => setShutdownTimeout(10000)).not.toThrow(); }); it('should handle negative timeout values', () => { // Should throw for negative values expect(() => setShutdownTimeout(-1000)).toThrow('Shutdown timeout must be positive'); }); it('should handle zero timeout', () => { // Should throw for zero timeout expect(() => setShutdownTimeout(0)).toThrow('Shutdown timeout must be positive'); }); }); describe('Status functions', () => { it('should report shutting down status correctly', async () => { expect(isShuttingDown()).toBe(false); const promise = initiateShutdown(); expect(isShuttingDown()).toBe(true); await promise; // Still true after completion expect(isShuttingDown()).toBe(true); resetShutdown(); expect(isShuttingDown()).toBe(false); }); it('should track shutdown signal', () => { expect(isShutdownSignalReceived()).toBe(false); // Simulate signal by setting global (global as any).__SHUTDOWN_SIGNAL_RECEIVED__ = true; expect(isShutdownSignalReceived()).toBe(true); // Clean up delete (global as any).__SHUTDOWN_SIGNAL_RECEIVED__; }); it('should count callbacks correctly', () => { expect(getShutdownCallbackCount()).toBe(0); onShutdown(async () => {}); expect(getShutdownCallbackCount()).toBe(1); onShutdownHigh(async () => {}); onShutdownMedium(async () => {}); onShutdownLow(async () => {}); expect(getShutdownCallbackCount()).toBe(4); }); }); describe('initiateShutdown', () => { it('should execute all callbacks', async () => { const callback1 = mock(async () => {}); const callback2 = mock(async () => {}); const callback3 = mock(async () => {}); onShutdown(callback1); onShutdown(callback2); onShutdown(callback3); const result = await initiateShutdown(); expect(callback1).toHaveBeenCalledTimes(1); expect(callback2).toHaveBeenCalledTimes(1); expect(callback3).toHaveBeenCalledTimes(1); expect(result.callbacksExecuted).toBe(3); expect(result.callbacksFailed).toBe(0); expect(result.success).toBe(true); }); it('should handle errors in callbacks', async () => { const successCallback = mock(async () => {}); const errorCallback = mock(async () => { throw new Error('Callback error'); }); onShutdown(successCallback, 'success-handler'); onShutdown(errorCallback, 'error-handler'); const result = await initiateShutdown(); expect(result.callbacksExecuted).toBe(2); expect(result.callbacksFailed).toBe(1); expect(result.success).toBe(false); expect(result.error).toContain('1 callbacks failed'); }); it('should only execute once', async () => { const callback = mock(async () => {}); onShutdown(callback); await initiateShutdown(); await initiateShutdown(); await initiateShutdown(); expect(callback).toHaveBeenCalledTimes(1); }); }); }); describe('Shutdown Class Direct Usage', () => { it('should create instance with options', () => { const options: ShutdownOptions = { timeout: 5000, autoRegister: false, }; const shutdown = new Shutdown(options); expect(shutdown).toBeInstanceOf(Shutdown); }); it('should handle concurrent callback registration', () => { const shutdown = new Shutdown(); const callbacks = Array.from({ length: 10 }, (_, i) => mock(async () => {})); // Register callbacks concurrently callbacks.forEach((cb, i) => { shutdown.onShutdown(cb, `handler-${i}`, i * 10); }); expect(shutdown.getCallbackCount()).toBe(10); }); it('should handle empty callback list', async () => { const shutdown = new Shutdown(); const result = await shutdown.shutdown(); expect(result.callbacksExecuted).toBe(0); expect(result.callbacksFailed).toBe(0); expect(result.success).toBe(true); }); it('should respect timeout', async () => { const shutdown = new Shutdown({ timeout: 100 }); const slowCallback = mock(async () => { await new Promise(resolve => setTimeout(resolve, 200)); }); shutdown.onShutdown(slowCallback, 'slow-handler'); const startTime = Date.now(); const result = await shutdown.shutdown(); const duration = Date.now() - startTime; expect(duration).toBeLessThan(150); // Should timeout before 200ms expect(result.success).toBe(false); expect(result.error).toContain('Shutdown timeout'); }); it('should handle synchronous callbacks', async () => { const shutdown = new Shutdown(); const syncCallback = mock(() => { // Synchronous callback return undefined; }); shutdown.onShutdown(syncCallback as any, 'sync-handler'); const result = await shutdown.shutdown(); expect(result.callbacksExecuted).toBe(1); expect(result.callbacksFailed).toBe(0); expect(syncCallback).toHaveBeenCalled(); }); }); describe('Edge Cases', () => { it('should handle callback that adds more callbacks', async () => { const addingCallback = mock(async () => { // Try to add callback during shutdown onShutdown(async () => { // This should not execute }); }); onShutdown(addingCallback); const countBefore = getShutdownCallbackCount(); await initiateShutdown(); // The new callback should not be executed in this shutdown expect(addingCallback).toHaveBeenCalledTimes(1); }); it('should handle very large number of callbacks', async () => { const callbacks = Array.from({ length: 100 }, (_, i) => mock(async () => {})); callbacks.forEach((cb, i) => { onShutdown(cb, `handler-${i}`, i); }); expect(getShutdownCallbackCount()).toBe(100); const result = await initiateShutdown(); expect(result.callbacksExecuted).toBe(100); expect(result.callbacksFailed).toBe(0); callbacks.forEach(cb => { expect(cb).toHaveBeenCalledTimes(1); }); }); it('should handle callbacks with same priority', async () => { const order: string[] = []; const callback1 = mock(async () => { order.push('1'); }); const callback2 = mock(async () => { order.push('2'); }); const callback3 = mock(async () => { order.push('3'); }); // All with same priority onShutdown(callback1, 'handler-1', 50); onShutdown(callback2, 'handler-2', 50); onShutdown(callback3, 'handler-3', 50); await initiateShutdown(); // Should execute all, order between same priority is not guaranteed expect(order).toHaveLength(3); expect(order).toContain('1'); expect(order).toContain('2'); expect(order).toContain('3'); }); it('should handle callback that throws non-Error', async () => { const throwingCallback = mock(async () => { throw 'string error'; // Non-Error thrown }); onShutdown(throwingCallback, 'throwing-handler'); const result = await initiateShutdown(); expect(result.callbacksFailed).toBe(1); expect(result.error).toContain('1 callbacks failed'); }); it('should handle undefined callback name', () => { const callback = mock(async () => {}); onShutdown(callback, undefined as any); expect(getShutdownCallbackCount()).toBe(1); }); }); describe('ShutdownResult Accuracy', () => { it('should provide accurate timing information', async () => { const delays = [10, 20, 30]; const callbacks = delays.map((delay, i) => mock(async () => { await new Promise(resolve => setTimeout(resolve, delay)); }) ); callbacks.forEach((cb, i) => { onShutdown(cb, `timer-${i}`); }); const startTime = Date.now(); const result = await initiateShutdown(); const totalTime = Date.now() - startTime; expect(result.duration).toBeGreaterThan(0); expect(result.duration).toBeLessThanOrEqual(totalTime); expect(result.success).toBe(true); }); it('should track individual callback execution', async () => { const successCount = 3; const errorCount = 2; for (let i = 0; i < successCount; i++) { onShutdown(async () => {}, `success-${i}`); } for (let i = 0; i < errorCount; i++) { onShutdown(async () => { throw new Error(`Error ${i}`); }, `error-${i}`); } const result = await initiateShutdown(); expect(result.callbacksExecuted).toBe(successCount + errorCount); expect(result.callbacksFailed).toBe(errorCount); expect(result.success).toBe(false); expect(result.error).toContain(`${errorCount} callbacks failed`); }); }); describe('Global State Management', () => { it('should properly reset global state', () => { // Add some callbacks onShutdown(async () => {}); onShutdownHigh(async () => {}); onShutdownLow(async () => {}); expect(getShutdownCallbackCount()).toBe(3); resetShutdown(); expect(getShutdownCallbackCount()).toBe(0); expect(isShuttingDown()).toBe(false); }); it('should maintain singleton across imports', () => { const instance1 = Shutdown.getInstance(); const instance2 = Shutdown.getInstance(); expect(instance1).toBe(instance2); }); }); });