177 lines
5.3 KiB
TypeScript
177 lines
5.3 KiB
TypeScript
// src/services/worker.test.ts
|
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
|
|
// --- Hoisted Mocks ---
|
|
const mocks = vi.hoisted(() => {
|
|
return {
|
|
gracefulShutdown: vi.fn(),
|
|
logger: {
|
|
info: vi.fn(),
|
|
error: vi.fn(),
|
|
warn: vi.fn(),
|
|
debug: vi.fn(),
|
|
},
|
|
// Mock process events
|
|
processOn: vi.fn(),
|
|
processExit: vi.fn(),
|
|
};
|
|
});
|
|
|
|
// --- Mock Modules ---
|
|
vi.mock('./workers.server', () => ({
|
|
gracefulShutdown: mocks.gracefulShutdown,
|
|
}));
|
|
|
|
vi.mock('./logger.server', () => ({
|
|
logger: mocks.logger,
|
|
}));
|
|
|
|
describe('Worker Entry Point', () => {
|
|
let originalProcessOn: typeof process.on;
|
|
let originalProcessExit: typeof process.exit;
|
|
let eventHandlers: Record<string, (...args: any[]) => void> = {};
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
vi.resetModules(); // This is key to re-run the top-level code in worker.ts
|
|
|
|
// Reset default mock implementations
|
|
mocks.gracefulShutdown.mockResolvedValue(undefined);
|
|
|
|
// Spy on and mock process methods
|
|
originalProcessOn = process.on;
|
|
originalProcessExit = process.exit;
|
|
|
|
// Capture event handlers registered with process.on
|
|
eventHandlers = {};
|
|
process.on = vi.fn((event, listener) => {
|
|
eventHandlers[event] = listener;
|
|
return process;
|
|
}) as any;
|
|
|
|
process.exit = mocks.processExit as any;
|
|
});
|
|
|
|
afterEach(() => {
|
|
// Restore original process methods
|
|
process.on = originalProcessOn;
|
|
process.exit = originalProcessExit;
|
|
});
|
|
|
|
it('should log initialization messages on import', async () => {
|
|
// Act: Import the module to trigger top-level code
|
|
await import('./worker');
|
|
|
|
// Assert
|
|
expect(mocks.logger.info).toHaveBeenCalledWith('[Worker] Initializing worker process...');
|
|
expect(mocks.logger.info).toHaveBeenCalledWith(
|
|
'[Worker] Worker process is running and listening for jobs.',
|
|
);
|
|
});
|
|
|
|
it('should register handlers for SIGINT, SIGTERM, uncaughtException, and unhandledRejection', async () => {
|
|
// Act
|
|
await import('./worker');
|
|
|
|
// Assert
|
|
expect(process.on).toHaveBeenCalledWith('SIGINT', expect.any(Function));
|
|
expect(process.on).toHaveBeenCalledWith('SIGTERM', expect.any(Function));
|
|
expect(process.on).toHaveBeenCalledWith('uncaughtException', expect.any(Function));
|
|
expect(process.on).toHaveBeenCalledWith('unhandledRejection', expect.any(Function));
|
|
});
|
|
|
|
describe('Shutdown Handling', () => {
|
|
it('should call gracefulShutdown on SIGINT', async () => {
|
|
// Arrange
|
|
await import('./worker');
|
|
const sigintHandler = eventHandlers['SIGINT'];
|
|
expect(sigintHandler).toBeDefined();
|
|
|
|
// Act
|
|
sigintHandler();
|
|
|
|
// Assert
|
|
expect(mocks.logger.info).toHaveBeenCalledWith(
|
|
'[Worker] Received SIGINT. Initiating graceful shutdown...',
|
|
);
|
|
expect(mocks.gracefulShutdown).toHaveBeenCalledWith('SIGINT');
|
|
});
|
|
|
|
it('should call gracefulShutdown on SIGTERM', async () => {
|
|
// Arrange
|
|
await import('./worker');
|
|
const sigtermHandler = eventHandlers['SIGTERM'];
|
|
expect(sigtermHandler).toBeDefined();
|
|
|
|
// Act
|
|
sigtermHandler();
|
|
|
|
// Assert
|
|
expect(mocks.logger.info).toHaveBeenCalledWith(
|
|
'[Worker] Received SIGTERM. Initiating graceful shutdown...',
|
|
);
|
|
expect(mocks.gracefulShutdown).toHaveBeenCalledWith('SIGTERM');
|
|
});
|
|
|
|
it('should log an error and exit if gracefulShutdown rejects', async () => {
|
|
// Arrange
|
|
const shutdownError = new Error('Shutdown failed');
|
|
mocks.gracefulShutdown.mockRejectedValue(shutdownError);
|
|
await import('./worker');
|
|
const sigintHandler = eventHandlers['SIGINT'];
|
|
|
|
// Act
|
|
// The handler catches the rejection, so we don't need to wrap this in expect().rejects
|
|
await sigintHandler();
|
|
|
|
// Assert
|
|
expect(mocks.logger.error).toHaveBeenCalledWith(
|
|
{ err: shutdownError },
|
|
'[Worker] Error during shutdown.',
|
|
);
|
|
expect(mocks.processExit).toHaveBeenCalledWith(1);
|
|
});
|
|
});
|
|
|
|
describe('Error Handling', () => {
|
|
it('should log uncaught exceptions', async () => {
|
|
// Arrange
|
|
await import('./worker');
|
|
const exceptionHandler = eventHandlers['uncaughtException'];
|
|
expect(exceptionHandler).toBeDefined();
|
|
const testError = new Error('Test uncaught exception');
|
|
|
|
// Act
|
|
exceptionHandler(testError);
|
|
|
|
// Assert
|
|
expect(mocks.logger.error).toHaveBeenCalledWith(
|
|
{ err: testError },
|
|
'[Worker] Uncaught exception',
|
|
);
|
|
});
|
|
|
|
it('should log unhandled promise rejections', async () => {
|
|
// Arrange
|
|
await import('./worker');
|
|
const rejectionHandler = eventHandlers['unhandledRejection'];
|
|
expect(rejectionHandler).toBeDefined();
|
|
const testReason = 'Promise rejected';
|
|
const testPromise = Promise.reject(testReason);
|
|
// We must handle this rejection in the test to prevent Vitest/Node from flagging it as unhandled
|
|
testPromise.catch((err) => {
|
|
console.log('Handled expected test rejection to prevent test runner error:', err);
|
|
});
|
|
|
|
// Act
|
|
rejectionHandler(testReason, testPromise);
|
|
|
|
// Assert
|
|
expect(mocks.logger.error).toHaveBeenCalledWith(
|
|
{ reason: testReason, promise: testPromise },
|
|
'[Worker] Unhandled Rejection',
|
|
);
|
|
});
|
|
});
|
|
});
|