import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { registerGracefulShutdown } from "./shutdown.js"; import type { Logger } from "@/observability/logger/types.js"; function createMockLogger(): Logger & { flush: ReturnType } { const logger = { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn(), child: vi.fn(), flush: vi.fn().mockResolvedValue(undefined), }; logger.child.mockReturnValue(logger); return logger as Logger & { flush: ReturnType }; } function createMockServer() { return { close: vi.fn((cb: () => void) => cb()), }; } describe("registerGracefulShutdown", () => { /** Captured SIGTERM handlers from process.on */ let capturedHandlers: Array<() => void>; let exitCalls: number[]; beforeEach(() => { capturedHandlers = []; exitCalls = []; // Intercept process.on("SIGTERM", handler) without fighting spy types process.on = ((event: string, listener: () => void) => { if (event === "SIGTERM") { capturedHandlers.push(listener); } return process; }) as typeof process.on; // Intercept process.exit process.exit = ((code?: number) => { exitCalls.push(code ?? 0); }) as typeof process.exit; vi.useFakeTimers(); }); afterEach(() => { vi.restoreAllMocks(); vi.useRealTimers(); }); function triggerSigterm(): void { expect(capturedHandlers.length).toBeGreaterThan(0); const handler = capturedHandlers[0]; if (handler) handler(); } it("registers a SIGTERM handler", () => { const logger = createMockLogger(); const server = createMockServer(); registerGracefulShutdown({ logger, server }); expect(capturedHandlers).toHaveLength(1); }); it("calls server.close on SIGTERM", async () => { const logger = createMockLogger(); const server = createMockServer(); registerGracefulShutdown({ logger, server }); triggerSigterm(); await vi.advanceTimersByTimeAsync(0); expect(server.close).toHaveBeenCalled(); }); it("flushes logger after server closes", async () => { const logger = createMockLogger(); const server = createMockServer(); registerGracefulShutdown({ logger, server }); triggerSigterm(); await vi.advanceTimersByTimeAsync(0); expect(logger.flush).toHaveBeenCalled(); }); it("exits with code 0 after successful drain", async () => { const logger = createMockLogger(); const server = createMockServer(); registerGracefulShutdown({ logger, server }); triggerSigterm(); await vi.advanceTimersByTimeAsync(0); expect(exitCalls).toContain(0); }); it("force-exits with code 1 if drain times out", async () => { const logger = createMockLogger(); const server = { close: vi.fn(), // never calls callback — simulates stuck connections }; registerGracefulShutdown({ logger, server, drainTimeoutMs: 1000 }); triggerSigterm(); // Advance past the drain timeout await vi.advanceTimersByTimeAsync(1100); expect(exitCalls).toContain(1); }); it("logs shutdown lifecycle events", async () => { const logger = createMockLogger(); const server = createMockServer(); registerGracefulShutdown({ logger, server }); triggerSigterm(); await vi.advanceTimersByTimeAsync(0); expect(logger.info).toHaveBeenCalledWith( "SIGTERM received, starting graceful shutdown", expect.any(Object), ); expect(logger.info).toHaveBeenCalledWith( "Server closed, flushing logs", undefined, ); }); it("uses default drainTimeoutMs of 30000", () => { const logger = createMockLogger(); const server = { close: vi.fn(), // never calls callback }; registerGracefulShutdown({ logger, server }); triggerSigterm(); // Shouldn't have exited yet at 29s vi.advanceTimersByTime(29_000); expect(exitCalls).toHaveLength(0); // Should force-exit at 30s vi.advanceTimersByTime(2_000); expect(exitCalls).toContain(1); }); });