File size: 4,622 Bytes
6c3af4e
 
e759542
ccc76d6
 
 
 
6c3af4e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ccc76d6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e759542
 
 
 
 
 
 
 
 
 
 
 
 
af13576
 
 
 
 
 
 
 
 
 
 
6c3af4e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
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<void>;

type RecordedResponse = ServerResponse & {
  setHeader: ReturnType<typeof vi.fn>;
  end: ReturnType<typeof vi.fn>;
};

function callStatus(): Promise<Record<string, unknown>> {
  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<typeof statusEndpointServerHook>[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();
    }
  });
});