Fix type errors and lint warnings in health and shutdown modules

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.
This commit is contained in:
2026-04-15 00:59:59 +03:00
parent 56cc9e1af2
commit 7db39cbeec
3 changed files with 63 additions and 72 deletions
+18 -14
View File
@@ -33,6 +33,10 @@ function createMockRes() {
return res;
}
function createRequest() {
return {} as Parameters<ReturnType<typeof healthMiddleware>>[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();
});
+8 -6
View File
@@ -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<void> => {
// Attempt to ping the upstream API
+37 -52
View File
@@ -22,30 +22,48 @@ function createMockServer() {
}
describe("registerGracefulShutdown", () => {
let processOnSpy: ReturnType<typeof vi.spyOn>;
let processExitSpy: ReturnType<typeof vi.spyOn>;
/** 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);
});
});