7db39cbeec
Use proper type-safe interfaces instead of Node.js http types for the health handler, and avoid vi.spyOn type issues in shutdown tests by directly intercepting process.on.
157 lines
4.0 KiB
TypeScript
157 lines
4.0 KiB
TypeScript
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<typeof vi.fn> } {
|
|
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<typeof vi.fn> };
|
|
}
|
|
|
|
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);
|
|
});
|
|
});
|