added cli-covarage tool and fixed more tests

This commit is contained in:
Boki 2025-06-26 14:23:01 -04:00
parent b63e58784c
commit b845a8eade
57 changed files with 11917 additions and 295 deletions

View file

@ -0,0 +1,66 @@
import { describe, it, expect } from 'bun:test';
import * as shutdownExports from '../src';
import { Shutdown } from '../src';
describe('Shutdown Package Exports', () => {
it('should export all main functions', () => {
expect(shutdownExports.onShutdown).toBeDefined();
expect(shutdownExports.onShutdownHigh).toBeDefined();
expect(shutdownExports.onShutdownMedium).toBeDefined();
expect(shutdownExports.onShutdownLow).toBeDefined();
expect(shutdownExports.setShutdownTimeout).toBeDefined();
expect(shutdownExports.isShuttingDown).toBeDefined();
expect(shutdownExports.isShutdownSignalReceived).toBeDefined();
expect(shutdownExports.getShutdownCallbackCount).toBeDefined();
expect(shutdownExports.initiateShutdown).toBeDefined();
expect(shutdownExports.shutdownAndExit).toBeDefined();
expect(shutdownExports.resetShutdown).toBeDefined();
});
it('should export Shutdown class', () => {
expect(shutdownExports.Shutdown).toBeDefined();
expect(shutdownExports.Shutdown).toBe(Shutdown);
});
it('should export correct function types', () => {
expect(typeof shutdownExports.onShutdown).toBe('function');
expect(typeof shutdownExports.onShutdownHigh).toBe('function');
expect(typeof shutdownExports.onShutdownMedium).toBe('function');
expect(typeof shutdownExports.onShutdownLow).toBe('function');
expect(typeof shutdownExports.setShutdownTimeout).toBe('function');
expect(typeof shutdownExports.isShuttingDown).toBe('function');
expect(typeof shutdownExports.isShutdownSignalReceived).toBe('function');
expect(typeof shutdownExports.getShutdownCallbackCount).toBe('function');
expect(typeof shutdownExports.initiateShutdown).toBe('function');
expect(typeof shutdownExports.shutdownAndExit).toBe('function');
expect(typeof shutdownExports.resetShutdown).toBe('function');
});
it('should export type definitions', () => {
// Type tests - these compile-time checks ensure types are exported
type TestShutdownCallback = shutdownExports.ShutdownCallback;
type TestShutdownOptions = shutdownExports.ShutdownOptions;
type TestShutdownResult = shutdownExports.ShutdownResult;
type TestPrioritizedShutdownCallback = shutdownExports.PrioritizedShutdownCallback;
// Runtime check that types can be used
const testCallback: TestShutdownCallback = async () => {};
const testOptions: TestShutdownOptions = { timeout: 5000, autoRegister: false };
const testResult: TestShutdownResult = {
success: true,
callbacksExecuted: 1,
callbacksFailed: 0,
duration: 100,
};
const testPrioritized: TestPrioritizedShutdownCallback = {
callback: testCallback,
priority: 50,
name: 'test',
};
expect(testCallback).toBeDefined();
expect(testOptions).toBeDefined();
expect(testResult).toBeDefined();
expect(testPrioritized).toBeDefined();
});
});

View file

@ -10,6 +10,7 @@ import {
onShutdownMedium,
resetShutdown,
setShutdownTimeout,
shutdownAndExit,
Shutdown,
} from '../src';
import type { ShutdownOptions, ShutdownResult } from '../src/types';
@ -103,12 +104,12 @@ describe('Shutdown Comprehensive Tests', () => {
it('should handle negative timeout values', () => {
// Should throw for negative values
expect(() => setShutdownTimeout(-1000)).toThrow('Shutdown timeout must be positive');
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 positive');
expect(() => setShutdownTimeout(0)).toThrow('Shutdown timeout must be a positive number');
});
});
@ -388,7 +389,7 @@ describe('Shutdown Comprehensive Tests', () => {
for (let i = 0; i < errorCount; i++) {
onShutdown(async () => {
throw new Error(`Error ${i}`);
throw new Error('Expected error');
}, `error-${i}`);
}
@ -397,30 +398,158 @@ describe('Shutdown Comprehensive Tests', () => {
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 () => {});
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;
expect(getShutdownCallbackCount()).toBe(3);
try {
const callback = mock(async () => {});
onShutdown(callback);
resetShutdown();
expect(getShutdownCallbackCount()).toBe(0);
expect(isShuttingDown()).toBe(false);
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 maintain singleton across imports', () => {
const instance1 = Shutdown.getInstance();
const instance2 = Shutdown.getInstance();
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;
expect(instance1).toBe(instance2);
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();
});
});
});

View file

@ -0,0 +1,254 @@
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__;
});
});
});