import type { IncomingMessage, ServerResponse } from "node:http"; import { describe, expect, it, vi } from "vitest"; import { getFallbackProviderStats } from "./fallbackSearchService.ts"; import { incrementSearchesFailedOnFallback, incrementSearchesServedByFallback, } from "./searchesSinceLastRestart.ts"; import { statusEndpointServerHook } from "./statusEndpointServerHook.ts"; // The health getters probe live services; this test is about the payload, // so they answer healthy without a network. vi.mock("./biEncoderService.ts", () => ({ getBiEncoderStatus: async () => true, })); vi.mock("./rerankerService.ts", () => ({ getRerankerStatus: async () => true, })); vi.mock("./webSearchService.ts", () => ({ getWebSearchServiceStatus: async () => "degraded", getSearchCircuitStats: () => ({ circuitState: "closed", circuitOpens: 0, }), })); type Handler = ( request: IncomingMessage, response: ServerResponse, next: () => void, ) => Promise; type RecordedResponse = ServerResponse & { setHeader: ReturnType; end: ReturnType; }; function callStatus(): Promise> { const use = vi.fn(); // Vite sets VITE_BUILD_DATE_TIME for every serve (see vite.config.ts). statusEndpointServerHook({ middlewares: { use }, config: { define: { VITE_BUILD_DATE_TIME: new Date().toISOString() } }, } as unknown as Parameters[0]); const response = { setHeader: vi.fn(), end: vi.fn(), } as unknown as RecordedResponse; const request = { url: "/status", headers: {}, } as unknown as IncomingMessage; return (use.mock.calls[0][0] as Handler)( request, response, () => undefined, ).then(() => JSON.parse(response.end.mock.calls[0][0])); } describe("statusEndpointServerHook", () => { it("publishes the web search verdict and the per-engine tally", async () => { const status = await callStatus(); // Passed through rather than recomputed: the hook used to derive this // field itself from a liveness probe, which reported an open circuit as // healthy. expect(status.webSearchServiceStatus).toBe("degraded"); expect(status.searches).toMatchObject({ degradedSearchTypes: [], unresponsiveEngines: {}, }); }); it("publishes the two fallback counters as plain counts", async () => { const before = await callStatus(); incrementSearchesServedByFallback(); incrementSearchesFailedOnFallback(); incrementSearchesFailedOnFallback(); const status = await callStatus(); // Exact values rather than expect.any(Number), which a pair of swapped // getters would satisfy too. The counters are module state that no test // here resets, so the deltas are what each getter moved. expect(status.searchesServedByFallback).toBe( (before.searchesServedByFallback as number) + 1, ); expect(status.searchesFailedOnFallback).toBe( (before.searchesFailedOnFallback as number) + 2, ); }); it("publishes the per-provider fallback counters under searches", async () => { const status = await callStatus(); expect(status.searches).toMatchObject({ fallbackProviders: getFallbackProviderStats(), }); expect( Object.keys( (status.searches as { fallbackProviders: object }).fallbackProviders, ), ).toEqual(["parallel", "youcom"]); }); it("publishes the page-read circuit count beside the outcome counters", async () => { const status = await callStatus(); // Counts and nothing else: the hosts the breaker boxes come from search // results, so naming one here would say something about a query. expect(status.pageReads).toMatchObject({ circuitOpens: expect.any(Number), skipped: { skippedByBreaker: expect.any(Number) }, }); }); it("keeps startedAt fixed at the restart while uptime advances", async () => { const first = await callStatus(); expect(typeof first.startedAt).toBe("string"); const startedAt = Date.parse(first.startedAt as string); expect(Number.isNaN(startedAt)).toBe(false); vi.useFakeTimers(); try { vi.setSystemTime(startedAt + 600_000); const later = await callStatus(); // The module captured its start once, on import: startedAt is the same // instant on every call while uptime advances. Replacing the capture // with a per-call Date.now() fails this. expect(later.startedAt).toBe(first.startedAt); expect(later.uptime).toBe("10 minutes"); } finally { vi.useRealTimers(); } }); });