diff --git a/src/server/routes/health.test.ts b/src/server/routes/health.test.ts index 14c12361..aa8aa116 100644 --- a/src/server/routes/health.test.ts +++ b/src/server/routes/health.test.ts @@ -33,6 +33,10 @@ function createMockRes() { return res; } +function createRequest() { + return {} as Parameters>[0]; +} + describe("healthMiddleware", () => { beforeEach(() => { vi.useFakeTimers(); @@ -42,11 +46,11 @@ describe("healthMiddleware", () => { const apiClient = createMockApiClient("ok"); const handler = healthMiddleware({ apiClient }); - const req = {} as any; + const req = createRequest(); const res = createMockRes(); const next = vi.fn(); - await handler(req, res as any, next); + await handler(req, res, next); expect(res.status).toHaveBeenCalledWith(200); expect(res.json).toHaveBeenCalledWith({ status: "ok" }); @@ -56,11 +60,11 @@ describe("healthMiddleware", () => { const apiClient = createMockApiClient("error"); const handler = healthMiddleware({ apiClient }); - const req = {} as any; + const req = createRequest(); const res = createMockRes(); const next = vi.fn(); - await handler(req, res as any, next); + await handler(req, res, next); expect(res.status).toHaveBeenCalledWith(503); expect(res.json).toHaveBeenCalledWith({ @@ -73,9 +77,9 @@ describe("healthMiddleware", () => { const apiClient = createMockApiClient("ok"); const handler = healthMiddleware({ apiClient }); - const req = {} as any; + const req = createRequest(); const res1 = createMockRes(); - await handler(req, res1 as any, vi.fn()); + await handler(req, res1, vi.fn()); expect(res1.status).toHaveBeenCalledWith(200); // Now make it fail @@ -85,7 +89,7 @@ describe("healthMiddleware", () => { vi.advanceTimersByTime(30_000); const res2 = createMockRes(); - await handler(req, res2 as any, vi.fn()); + await handler(req, res2, vi.fn()); expect(res2.status).toHaveBeenCalledWith(200); }); @@ -93,9 +97,9 @@ describe("healthMiddleware", () => { const apiClient = createMockApiClient("ok"); const handler = healthMiddleware({ apiClient }); - const req = {} as any; + const req = createRequest(); const res1 = createMockRes(); - await handler(req, res1 as any, vi.fn()); + await handler(req, res1, vi.fn()); expect(res1.status).toHaveBeenCalledWith(200); // Now make it fail @@ -105,7 +109,7 @@ describe("healthMiddleware", () => { vi.advanceTimersByTime(61_000); const res2 = createMockRes(); - await handler(req, res2 as any, vi.fn()); + await handler(req, res2, vi.fn()); expect(res2.status).toHaveBeenCalledWith(503); }); @@ -116,12 +120,12 @@ describe("healthMiddleware", () => { upstreamTimeoutMs: 100, }); - const req = {} as any; + const req = createRequest(); const res = createMockRes(); const next = vi.fn(); // The handler should abort the ping after 100ms - const promise = handler(req, res as any, next); + const promise = handler(req, res, next); vi.advanceTimersByTime(200); await promise; @@ -132,11 +136,11 @@ describe("healthMiddleware", () => { const apiClient = createMockApiClient("ok"); const handler = healthMiddleware({ apiClient }); - const req = {} as any; + const req = createRequest(); const res = createMockRes(); const next = vi.fn(); - await handler(req, res as any, next); + await handler(req, res, next); expect(next).not.toHaveBeenCalled(); }); diff --git a/src/server/routes/health.ts b/src/server/routes/health.ts index 2fbc92c1..c2150e63 100644 --- a/src/server/routes/health.ts +++ b/src/server/routes/health.ts @@ -1,11 +1,16 @@ import type { ApiClient } from "@/shared/api/client.js"; -import type { IncomingMessage, ServerResponse } from "node:http"; export interface HealthMiddlewareOptions { apiClient: ApiClient; upstreamTimeoutMs?: number; } +/** Express-style response with status/json helpers. */ +interface HealthResponse { + status(code: number): HealthResponse; + json(body: unknown): void; +} + const STALE_THRESHOLD_MS = 60_000; const DEFAULT_UPSTREAM_TIMEOUT_MS = 5_000; @@ -24,11 +29,8 @@ export function healthMiddleware(options: HealthMiddlewareOptions) { let lastSuccessTs = 0; return async ( - _req: IncomingMessage, - res: ServerResponse & { - status: (code: number) => ServerResponse; - json: (body: unknown) => void; - }, + _req: unknown, + res: HealthResponse, _next: () => void, ): Promise => { // Attempt to ping the upstream API diff --git a/src/server/shutdown.test.ts b/src/server/shutdown.test.ts index 7d4fe843..9c0f7c99 100644 --- a/src/server/shutdown.test.ts +++ b/src/server/shutdown.test.ts @@ -22,30 +22,48 @@ function createMockServer() { } describe("registerGracefulShutdown", () => { - let processOnSpy: ReturnType; - let processExitSpy: ReturnType; + /** Captured SIGTERM handlers from process.on */ + let capturedHandlers: Array<() => void>; + let exitCalls: number[]; beforeEach(() => { - processOnSpy = vi.spyOn(process, "on"); - processExitSpy = vi - .spyOn(process, "exit") - .mockImplementation(() => undefined as never); + 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(() => { - processOnSpy.mockRestore(); - processExitSpy.mockRestore(); + 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(processOnSpy).toHaveBeenCalledWith("SIGTERM", expect.any(Function)); + expect(capturedHandlers).toHaveLength(1); }); it("calls server.close on SIGTERM", async () => { @@ -53,16 +71,8 @@ describe("registerGracefulShutdown", () => { const server = createMockServer(); registerGracefulShutdown({ logger, server }); + triggerSigterm(); - // Extract and call the SIGTERM handler - const sigtermCall = processOnSpy.mock.calls.find( - (call) => call[0] === "SIGTERM", - ); - expect(sigtermCall).toBeDefined(); - const handler = sigtermCall![1] as () => void; - handler(); - - // Let microtasks resolve await vi.advanceTimersByTimeAsync(0); expect(server.close).toHaveBeenCalled(); @@ -73,12 +83,7 @@ describe("registerGracefulShutdown", () => { const server = createMockServer(); registerGracefulShutdown({ logger, server }); - - const sigtermCall = processOnSpy.mock.calls.find( - (call) => call[0] === "SIGTERM", - ); - const handler = sigtermCall![1] as () => void; - handler(); + triggerSigterm(); await vi.advanceTimersByTimeAsync(0); @@ -90,16 +95,11 @@ describe("registerGracefulShutdown", () => { const server = createMockServer(); registerGracefulShutdown({ logger, server }); - - const sigtermCall = processOnSpy.mock.calls.find( - (call) => call[0] === "SIGTERM", - ); - const handler = sigtermCall![1] as () => void; - handler(); + triggerSigterm(); await vi.advanceTimersByTimeAsync(0); - expect(processExitSpy).toHaveBeenCalledWith(0); + expect(exitCalls).toContain(0); }); it("force-exits with code 1 if drain times out", async () => { @@ -109,17 +109,12 @@ describe("registerGracefulShutdown", () => { }; registerGracefulShutdown({ logger, server, drainTimeoutMs: 1000 }); - - const sigtermCall = processOnSpy.mock.calls.find( - (call) => call[0] === "SIGTERM", - ); - const handler = sigtermCall![1] as () => void; - handler(); + triggerSigterm(); // Advance past the drain timeout await vi.advanceTimersByTimeAsync(1100); - expect(processExitSpy).toHaveBeenCalledWith(1); + expect(exitCalls).toContain(1); }); it("logs shutdown lifecycle events", async () => { @@ -127,12 +122,7 @@ describe("registerGracefulShutdown", () => { const server = createMockServer(); registerGracefulShutdown({ logger, server }); - - const sigtermCall = processOnSpy.mock.calls.find( - (call) => call[0] === "SIGTERM", - ); - const handler = sigtermCall![1] as () => void; - handler(); + triggerSigterm(); await vi.advanceTimersByTimeAsync(0); @@ -153,19 +143,14 @@ describe("registerGracefulShutdown", () => { }; registerGracefulShutdown({ logger, server }); - - const sigtermCall = processOnSpy.mock.calls.find( - (call) => call[0] === "SIGTERM", - ); - const handler = sigtermCall![1] as () => void; - handler(); + triggerSigterm(); // Shouldn't have exited yet at 29s vi.advanceTimersByTime(29_000); - expect(processExitSpy).not.toHaveBeenCalled(); + expect(exitCalls).toHaveLength(0); // Should force-exit at 30s vi.advanceTimersByTime(2_000); - expect(processExitSpy).toHaveBeenCalledWith(1); + expect(exitCalls).toContain(1); }); });