stock-bot/libs/core/shutdown/test/shutdown-comprehensive.test.ts
2025-06-26 16:12:27 -04:00

557 lines
16 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test';
import {
getShutdownCallbackCount,
initiateShutdown,
isShutdownSignalReceived,
isShuttingDown,
onShutdown,
onShutdownHigh,
onShutdownLow,
onShutdownMedium,
resetShutdown,
setShutdownTimeout,
Shutdown,
shutdownAndExit,
} 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 a positive number'
);
});
it('should handle zero timeout', () => {
// Should throw for zero timeout
expect(() => setShutdownTimeout(0)).toThrow('Shutdown timeout must be a positive number');
});
});
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('Expected error');
}, `error-${i}`);
}
const result = await initiateShutdown();
expect(result.callbacksExecuted).toBe(successCount + errorCount);
expect(result.callbacksFailed).toBe(errorCount);
expect(result.success).toBe(false);
});
});
describe('shutdownAndExit', () => {
it('should call process.exit after shutdown', async () => {
// Mock process.exit
const originalExit = process.exit;
const exitMock = mock(() => {
throw new Error('Process exit called');
});
process.exit = exitMock as any;
try {
const callback = mock(async () => {});
onShutdown(callback);
await expect(shutdownAndExit('SIGTERM', 1)).rejects.toThrow('Process exit called');
expect(callback).toHaveBeenCalledTimes(1);
expect(exitMock).toHaveBeenCalledWith(1);
} finally {
// Restore process.exit
process.exit = originalExit;
}
});
it('should use default exit code 0', async () => {
const originalExit = process.exit;
const exitMock = mock(() => {
throw new Error('Process exit called');
});
process.exit = exitMock as any;
try {
await expect(shutdownAndExit()).rejects.toThrow('Process exit called');
expect(exitMock).toHaveBeenCalledWith(0);
} finally {
process.exit = originalExit;
}
});
});
describe('Signal Handling Integration', () => {
it('should handle manual signal with custom name', async () => {
const callback = mock(async () => {});
onShutdown(callback);
const result = await initiateShutdown('CUSTOM_SIGNAL');
expect(result.success).toBe(true);
expect(callback).toHaveBeenCalled();
});
it('should handle shutdown from getInstance without options', () => {
const instance = Shutdown.getInstance();
expect(instance).toBeInstanceOf(Shutdown);
// Call again to test singleton
const instance2 = Shutdown.getInstance();
expect(instance2).toBe(instance);
});
it('should handle global instance state correctly', () => {
// Start fresh
resetShutdown();
expect(getShutdownCallbackCount()).toBe(0);
// Add callback - this creates global instance
onShutdown(async () => {});
expect(getShutdownCallbackCount()).toBe(1);
// Reset and verify
resetShutdown();
expect(getShutdownCallbackCount()).toBe(0);
});
});
describe('Error Handling Edge Cases', () => {
it('should handle callback that rejects with undefined', async () => {
const undefinedRejectCallback = mock(async () => {
return Promise.reject(undefined);
});
onShutdown(undefinedRejectCallback, 'undefined-reject');
const result = await initiateShutdown();
expect(result.callbacksFailed).toBe(1);
expect(result.success).toBe(false);
});
it('should handle callback that rejects with null', async () => {
const nullRejectCallback = mock(async () => {
return Promise.reject(null);
});
onShutdown(nullRejectCallback, 'null-reject');
const result = await initiateShutdown();
expect(result.callbacksFailed).toBe(1);
expect(result.success).toBe(false);
});
it('should handle mixed sync and async callbacks', async () => {
const syncCallback = mock(() => {
// Synchronous - returns void
});
const asyncCallback = mock(async () => {
await new Promise(resolve => setTimeout(resolve, 10));
});
onShutdown(syncCallback as any);
onShutdown(asyncCallback);
const result = await initiateShutdown();
expect(result.callbacksExecuted).toBe(2);
expect(syncCallback).toHaveBeenCalled();
expect(asyncCallback).toHaveBeenCalled();
});
});
describe('Shutdown Method Variants', () => {
it('should handle direct priority parameter in onShutdown', () => {
const callback = mock(async () => {});
// Test with name and priority swapped (legacy support)
onShutdown(callback, 75, 'custom-name');
expect(getShutdownCallbackCount()).toBe(1);
});
it('should handle callback without any parameters', () => {
const callback = mock(async () => {});
onShutdown(callback);
expect(getShutdownCallbackCount()).toBe(1);
});
it('should validate setTimeout input', () => {
const shutdown = new Shutdown();
// Valid timeout
expect(() => shutdown.setTimeout(5000)).not.toThrow();
// Invalid timeouts should throw
expect(() => shutdown.setTimeout(-1)).toThrow();
expect(() => shutdown.setTimeout(0)).toThrow();
expect(() => shutdown.setTimeout(NaN)).toThrow();
});
});
});