stock-bot/libs/core/shutdown/test/shutdown-signals.test.ts

254 lines
No EOL
9 KiB
TypeScript

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<string, Function[]> = {};
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__;
});
});
});