/** * apps/worker — unit test: GET /health/search-index:lag (TASK-0100 S4). * * Covers the three wiring states of the consumer-lag probe (`checkSearchIndexLag`) * against the REAL health server (node:http on an ephemeral port, no Redis): * * 1. wired → 200, available:true, backlog === pending + lagEntries * 2. probe-absent → 200, available:false, zeroed counts (callers skip) * 3. probe-throws → 200, available:false, zeroed counts (soft-fail; the * endpoint is observability and NEVER 5xx's) * * Plus: a hanging probe (past the 1.5 s probe timeout) also soft-fails to * available:false, and liveness (/health) stays 200 in every case. * * NOTE on the status code: server.ts's docstring says "503 { available:false } * when the probe fails", but the implementation (authoritative) always answers * 200 with `available:false`. The e2e util (tests/e2e/utils/search-lag.ts * `getSearchIndexLag`) treats any non-200 OR `available:false` as "unavailable", * so both read the same contract; this test pins the implemented behavior. */ import type { Server } from "node:http"; import { describe, expect, it } from "vitest"; import { createHealthServer, type SearchIndexLagResult } from "../health/server.js"; interface LagBody extends SearchIndexLagResult {} interface Started { server: Server; base: string; } /** Start the health server on an ephemeral 127.0.0.1 port. */ async function start(deps: Parameters[0]): Promise { const server = createHealthServer(deps); await new Promise((resolve, reject) => { server.once("error", reject); server.listen(0, "127.0.0.1", () => resolve()); }); const addr = server.address(); if (addr === null || typeof addr === "string") throw new Error("no ephemeral port"); return { server, base: `http://127.0.0.1:${addr.port}` }; } async function stop(s: Started): Promise { await new Promise((resolve) => { s.server.closeAllConnections?.(); s.server.close(() => resolve()); }); } async function getLag(base: string): Promise<{ status: number; body: LagBody }> { const res = await fetch(`${base}/health/search-index:lag`); return { status: res.status, body: (await res.json()) as LagBody }; } async function getHealth(base: string): Promise { const res = await fetch(`${base}/health`); return res.status; } const pingOk = async (): Promise => true; describe("GET /health/search-index:lag (TASK-0100 S4)", () => { it("wired probe → 200 available:true with backlog = pending + lagEntries", async () => { const s = await start({ pingRedis: pingOk, checkSearchIndexLag: async () => ({ pending: 3, lagEntries: 2 }), }); try { const { status, body } = await getLag(s.base); expect(status).toBe(200); expect(body.metric).toBe("search-index:lag"); expect(body.stream).toBe("factory:events"); expect(body.group).toBe("worker-search-index"); expect(body.available).toBe(true); expect(body.pending).toBe(3); expect(body.lagEntries).toBe(2); expect(body.backlog).toBe(5); // pending + lagEntries — the number the e2e drains to 0 expect(typeof body.timestamp).toBe("string"); // Liveness is independent of the probe and stays 200. expect(await getHealth(s.base)).toBe(200); } finally { await stop(s); } }); it("probe absent → 200 available:false with zeroed counts (callers skip)", async () => { const s = await start({ pingRedis: pingOk }); // no checkSearchIndexLag try { const { status, body } = await getLag(s.base); expect(status).toBe(200); expect(body.available).toBe(false); expect(body.pending).toBe(0); expect(body.lagEntries).toBe(0); expect(body.backlog).toBe(0); expect(await getHealth(s.base)).toBe(200); } finally { await stop(s); } }); it("probe throws → 200 available:false with zeroed counts (soft-fail, never 5xx)", async () => { const s = await start({ pingRedis: pingOk, checkSearchIndexLag: async () => { throw new Error("redis down"); }, }); try { const { status, body } = await getLag(s.base); expect(status).toBe(200); expect(body.available).toBe(false); expect(body.pending).toBe(0); expect(body.lagEntries).toBe(0); expect(body.backlog).toBe(0); expect(await getHealth(s.base)).toBe(200); } finally { await stop(s); } }); it("hanging probe (past the 1.5 s timeout) → 200 available:false (soft-fail)", async () => { const s = await start({ pingRedis: pingOk, checkSearchIndexLag: () => new Promise(() => {}), // never settles }); try { const t0 = Date.now(); const { status, body } = await getLag(s.base); // Resolves on the probe timeout (1.5 s inside `lag()`), well under 5 s. expect(status).toBe(200); expect(body.available).toBe(false); expect(body.backlog).toBe(0); expect(Date.now() - t0).toBeLessThan(4_000); } finally { await stop(s); } }); });