stock-bot/libs/core/shutdown/test/shutdown-signals.test.ts
2025-06-26 17:30:13 -04:00

219 lines
No EOL
6.8 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 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<string, Function[]> = {};
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();
});
});
});