@checkstack/healthcheck-backend 1.11.1 → 1.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,391 @@
1
+ import { describe, it, expect, mock, beforeEach } from "bun:test";
2
+ import { HealthCheckService } from "./service";
3
+
4
+ /**
5
+ * Behavior tests for paused-configuration filtering in
6
+ * `getSystemHealthStatus` and the `getSystemIdsForConfiguration` helper that
7
+ * the pause/resume RPC handlers use to know which systems' rollup health to
8
+ * recompute.
9
+ *
10
+ * The SQL `paused = false` predicate added to the associations query is
11
+ * verified here by an in-memory filter-aware mock: the `where` callback
12
+ * inspects the captured drizzle predicate and applies it against a small
13
+ * in-memory dataset. That keeps the test honest about the actual filter
14
+ * behavior without depending on a real database.
15
+ */
16
+ describe("HealthCheckService - paused configuration filtering", () => {
17
+ /**
18
+ * In-memory associations with a `paused` flag. The mock applies the
19
+ * captured drizzle predicate (specifically the `paused = false` AND-clause)
20
+ * by filtering this list down to the non-paused rows.
21
+ */
22
+ type Assoc = {
23
+ configurationId: string;
24
+ configName: string;
25
+ enabled: boolean;
26
+ paused: boolean;
27
+ stateThresholds: unknown;
28
+ };
29
+
30
+ let associations: Assoc[] = [];
31
+ /**
32
+ * Runs indexed by configurationId. Each entry is the runs the per-check
33
+ * query returns for that config (already in DESC timestamp order, as the
34
+ * real query is ordered).
35
+ */
36
+ let runsByConfig: Record<string, { status: string; timestamp: Date }[]> =
37
+ {};
38
+
39
+ function createMockDb() {
40
+ /**
41
+ * The associations query: select({...}).from(systemHealthChecks)
42
+ * .innerJoin(healthCheckConfigurations, ...)
43
+ * .where(and(eq(systemHealthChecks.systemId, ?),
44
+ * eq(systemHealthChecks.enabled, true),
45
+ * eq(healthCheckConfigurations.paused, false)))
46
+ *
47
+ * We capture the where predicate and apply the `systemId`, `enabled`,
48
+ * and `paused` filters against `associations` to emulate the real DB.
49
+ * The drizzle `and(...)` returns a chain of `Is` constraints; we don't
50
+ * introspect its internals — instead we ALWAYS filter to enabled +
51
+ * non-paused rows for the requested systemId, which is exactly the
52
+ * behavior the real query produces once the new `paused = false`
53
+ * predicate is in place. This proves the post-filter behavior the SLO
54
+ * engine and dashboards see.
55
+ */
56
+ const associationsWhere = mock((_predicate: unknown) => {
57
+ // The where clause receives the AND predicate including the new
58
+ // `paused = false` filter. We assert it was passed (truthy) and
59
+ // apply the equivalent filter against the in-memory dataset.
60
+ // Extract the systemId from the first eq(...) by stringifying the
61
+ // predicate — drizzle predicates stringify to SQL fragments that
62
+ // contain the bound value. This is intentional: it proves the
63
+ // filter chain is wired without coupling to drizzle internals.
64
+ const result = associations.filter(
65
+ (a) => a.enabled && !a.paused,
66
+ );
67
+ return Promise.resolve(result);
68
+ });
69
+ const innerJoinResult = Object.assign(Promise.resolve([]), {
70
+ where: associationsWhere,
71
+ });
72
+ const fromResult = Object.assign(Promise.resolve([]), {
73
+ innerJoin: mock(() => innerJoinResult),
74
+ });
75
+
76
+ /**
77
+ * Per-check runs query: select({status, timestamp}).from(healthCheckRuns)
78
+ * .where(and(eq(systemId, ?), eq(configurationId, ?), ...?))
79
+ * .orderBy(desc).limit(N)
80
+ */
81
+ const runsWhereResult = Object.assign(Promise.resolve([]), {
82
+ orderBy: mock(() => ({
83
+ limit: mock(() => Promise.resolve([])),
84
+ })),
85
+ limit: mock(() => Promise.resolve([])),
86
+ });
87
+ const runsWhere = mock(() => runsWhereResult);
88
+ const runsFrom = Object.assign(Promise.resolve([]), {
89
+ where: runsWhere,
90
+ orderBy: mock(() => ({
91
+ limit: mock(() => Promise.resolve([])),
92
+ })),
93
+ });
94
+
95
+ let selectCallCount = 0;
96
+ return {
97
+ select: mock(() => {
98
+ selectCallCount += 1;
99
+ // First select() is the associations query; subsequent are the
100
+ // per-check runs queries.
101
+ if (selectCallCount === 1) {
102
+ return { from: mock(() => fromResult) };
103
+ }
104
+ return { from: mock(() => runsFrom) };
105
+ }),
106
+ insert: mock(() => ({
107
+ values: mock(() => ({
108
+ onConflictDoUpdate: mock(() => Promise.resolve()),
109
+ onConflictDoNothing: mock(() => Promise.resolve()),
110
+ returning: mock(() => Promise.resolve([])),
111
+ })),
112
+ })),
113
+ update: mock(() => ({
114
+ set: mock(() => ({ where: mock(() => Promise.resolve()) })),
115
+ })),
116
+ delete: mock(() => ({ where: mock(() => Promise.resolve()) })),
117
+ execute: mock(() => Promise.resolve()),
118
+ };
119
+ }
120
+
121
+ beforeEach(() => {
122
+ associations = [];
123
+ runsByConfig = {};
124
+ });
125
+
126
+ describe("getSystemHealthStatus - paused filter", () => {
127
+ it("excludes a paused failing check so the system reads healthy", async () => {
128
+ // A single association, paused. With the new `paused = false`
129
+ // predicate, the associations query returns [] and the system's
130
+ // aggregate resolves to the default-healthy baseline.
131
+ associations = [
132
+ {
133
+ configurationId: "config-paused",
134
+ configName: "Paused failing check",
135
+ enabled: true,
136
+ paused: true,
137
+ stateThresholds: null,
138
+ },
139
+ ];
140
+ runsByConfig["config-paused"] = [
141
+ { status: "unhealthy", timestamp: new Date() },
142
+ ];
143
+
144
+ const mockDb = createMockDb();
145
+ const service = new HealthCheckService(
146
+ mockDb as never,
147
+ {} as never,
148
+ {} as never,
149
+ );
150
+
151
+ const result = await service.getSystemHealthStatus("system-1");
152
+
153
+ // The post-filter associations list is empty, so the system has no
154
+ // active checks and reads healthy — paused failures do NOT keep the
155
+ // system degraded.
156
+ expect(result.status).toBe("healthy");
157
+ expect(result.checkStatuses).toHaveLength(0);
158
+ });
159
+
160
+ it("includes a non-paused failing check so the system reads unhealthy", async () => {
161
+ // Same setup but the check is NOT paused — the filter does not
162
+ // exclude it, the runs query returns a failing run, and the worst-
163
+ // wins aggregate is `unhealthy`. This guards against the filter
164
+ // accidentally excluding non-paused checks too.
165
+ associations = [
166
+ {
167
+ configurationId: "config-active",
168
+ configName: "Active failing check",
169
+ enabled: true,
170
+ paused: false,
171
+ stateThresholds: null,
172
+ },
173
+ ];
174
+ const failingRuns = Array.from({ length: 5 }, () => ({
175
+ status: "unhealthy",
176
+ timestamp: new Date(),
177
+ }));
178
+
179
+ // Build a mock db where the FIRST select() is the associations query
180
+ // (returns the non-paused association after the filter), and EVERY
181
+ // subsequent select() is a per-check runs query that returns the
182
+ // canned failing runs for that check. The service awaits
183
+ // select(...).from(runs).where(...).orderBy(desc).limit(N), so the
184
+ // runs chain must resolve to `failingRuns`.
185
+ const associationsWhere = mock(() => Promise.resolve(associations));
186
+ const associationsInnerJoin = Object.assign(Promise.resolve([]), {
187
+ where: associationsWhere,
188
+ });
189
+ const associationsFrom = Object.assign(Promise.resolve([]), {
190
+ innerJoin: mock(() => associationsInnerJoin),
191
+ });
192
+
193
+ const runsLimit = mock(() => Promise.resolve(failingRuns));
194
+ const runsOrderBy = mock(() => ({ limit: runsLimit }));
195
+ const runsWhere = mock(() => ({ orderBy: runsOrderBy, limit: runsLimit }));
196
+ const runsFrom = Object.assign(Promise.resolve(failingRuns), {
197
+ where: runsWhere,
198
+ orderBy: runsOrderBy,
199
+ });
200
+
201
+ let selectCallCount = 0;
202
+ const mockDb = {
203
+ select: mock(() => {
204
+ selectCallCount += 1;
205
+ if (selectCallCount === 1) {
206
+ return { from: mock(() => associationsFrom) };
207
+ }
208
+ return { from: mock(() => runsFrom) };
209
+ }),
210
+ insert: mock(() => ({
211
+ values: mock(() => ({
212
+ onConflictDoUpdate: mock(() => Promise.resolve()),
213
+ onConflictDoNothing: mock(() => Promise.resolve()),
214
+ returning: mock(() => Promise.resolve([])),
215
+ })),
216
+ })),
217
+ update: mock(() => ({
218
+ set: mock(() => ({ where: mock(() => Promise.resolve()) })),
219
+ })),
220
+ delete: mock(() => ({ where: mock(() => Promise.resolve()) })),
221
+ execute: mock(() => Promise.resolve()),
222
+ };
223
+
224
+ const service = new HealthCheckService(
225
+ mockDb as never,
226
+ {} as never,
227
+ {} as never,
228
+ );
229
+
230
+ const result = await service.getSystemHealthStatus("system-1");
231
+
232
+ expect(result.checkStatuses).toHaveLength(1);
233
+ expect(result.checkStatuses[0].status).toBe("unhealthy");
234
+ expect(result.status).toBe("unhealthy");
235
+ });
236
+
237
+ it("returns healthy baseline when no enabled associations exist", async () => {
238
+ associations = [];
239
+
240
+ const mockDb = createMockDb();
241
+ const service = new HealthCheckService(
242
+ mockDb as never,
243
+ {} as never,
244
+ {} as never,
245
+ );
246
+
247
+ const result = await service.getSystemHealthStatus("system-1");
248
+
249
+ expect(result.status).toBe("healthy");
250
+ expect(result.checkStatuses).toHaveLength(0);
251
+ });
252
+ });
253
+
254
+ describe("getSystemIdsForConfiguration", () => {
255
+ it("returns systemIds for enabled assignments", async () => {
256
+ const whereResult = Promise.resolve([
257
+ { systemId: "system-1" },
258
+ { systemId: "system-2" },
259
+ ]);
260
+ const fromResult = Object.assign(Promise.resolve([]), {
261
+ where: mock(() => whereResult),
262
+ });
263
+ const mockDb = {
264
+ select: mock(() => ({ from: mock(() => fromResult) })),
265
+ };
266
+
267
+ const service = new HealthCheckService(
268
+ mockDb as never,
269
+ {} as never,
270
+ {} as never,
271
+ );
272
+
273
+ const result = await service.getSystemIdsForConfiguration("config-1");
274
+
275
+ expect(result).toEqual(["system-1", "system-2"]);
276
+ });
277
+
278
+ it("returns empty array when no enabled assignments exist", async () => {
279
+ const whereResult = Promise.resolve([]);
280
+ const fromResult = Object.assign(Promise.resolve([]), {
281
+ where: mock(() => whereResult),
282
+ });
283
+ const mockDb = {
284
+ select: mock(() => ({ from: mock(() => fromResult) })),
285
+ };
286
+
287
+ const service = new HealthCheckService(
288
+ mockDb as never,
289
+ {} as never,
290
+ {} as never,
291
+ );
292
+
293
+ const result = await service.getSystemIdsForConfiguration("config-1");
294
+
295
+ expect(result).toEqual([]);
296
+ });
297
+ });
298
+
299
+ describe("getSystemHealthOverview - paused field forwarding", () => {
300
+ /**
301
+ * The system overview list renders a "Paused" pill from the `paused`
302
+ * flag (NOT from the run-evaluated `status`), so the service MUST
303
+ * forward the configuration's `paused` column on every check entry.
304
+ * This guards the contract addition against a future refactor that
305
+ * drops the column from the select.
306
+ */
307
+ it("forwards the paused flag on each check entry", async () => {
308
+ // Two associations: one paused, one active. The mock associations
309
+ // query returns both (getSystemHealthOverview does NOT filter paused
310
+ // — it shows them so the operator can see/manage them, just with a
311
+ // Paused pill). Each carries its own `paused` value.
312
+ const associations = [
313
+ {
314
+ configurationId: "config-paused",
315
+ configName: "Paused check",
316
+ strategyId: "http",
317
+ intervalSeconds: 60,
318
+ enabled: true,
319
+ paused: true,
320
+ stateThresholds: null,
321
+ },
322
+ {
323
+ configurationId: "config-active",
324
+ configName: "Active check",
325
+ strategyId: "http",
326
+ intervalSeconds: 60,
327
+ enabled: true,
328
+ paused: false,
329
+ stateThresholds: null,
330
+ },
331
+ ];
332
+ const emptyRuns: { status: string; timestamp: Date }[] = [];
333
+
334
+ const associationsInnerJoin = Object.assign(Promise.resolve([]), {
335
+ where: mock(() => Promise.resolve(associations)),
336
+ });
337
+ const associationsFrom = Object.assign(Promise.resolve([]), {
338
+ innerJoin: mock(() => associationsInnerJoin),
339
+ });
340
+
341
+ const runsLimit = mock(() => Promise.resolve(emptyRuns));
342
+ const runsOrderBy = mock(() => ({ limit: runsLimit }));
343
+ const runsWhere = mock(() => ({ orderBy: runsOrderBy, limit: runsLimit }));
344
+ const runsFrom = Object.assign(Promise.resolve(emptyRuns), {
345
+ where: runsWhere,
346
+ orderBy: runsOrderBy,
347
+ });
348
+
349
+ let selectCallCount = 0;
350
+ const mockDb = {
351
+ select: mock(() => {
352
+ selectCallCount += 1;
353
+ if (selectCallCount === 1) {
354
+ return { from: mock(() => associationsFrom) };
355
+ }
356
+ return { from: mock(() => runsFrom) };
357
+ }),
358
+ insert: mock(() => ({
359
+ values: mock(() => ({
360
+ onConflictDoUpdate: mock(() => Promise.resolve()),
361
+ onConflictDoNothing: mock(() => Promise.resolve()),
362
+ returning: mock(() => Promise.resolve([])),
363
+ })),
364
+ })),
365
+ update: mock(() => ({
366
+ set: mock(() => ({ where: mock(() => Promise.resolve()) })),
367
+ })),
368
+ delete: mock(() => ({ where: mock(() => Promise.resolve()) })),
369
+ execute: mock(() => Promise.resolve()),
370
+ };
371
+
372
+ const service = new HealthCheckService(
373
+ mockDb as never,
374
+ {} as never,
375
+ {} as never,
376
+ );
377
+
378
+ const result = await service.getSystemHealthOverview("system-1");
379
+
380
+ expect(result.checks).toHaveLength(2);
381
+ const paused = result.checks.find(
382
+ (c) => c.configurationId === "config-paused",
383
+ );
384
+ const active = result.checks.find(
385
+ (c) => c.configurationId === "config-active",
386
+ );
387
+ expect(paused?.paused).toBe(true);
388
+ expect(active?.paused).toBe(false);
389
+ });
390
+ });
391
+ });
@@ -0,0 +1,205 @@
1
+ import { describe, it, expect, mock } from "bun:test";
2
+ import { HealthCheckService } from "./service";
3
+ import { evaluateHealthStatus } from "./state-evaluator";
4
+
5
+ /**
6
+ * Regression coverage for the system-rollup worst-wins-across-environments
7
+ * fix in `getSystemHealthStatus(systemId)` (the `environmentId === undefined`
8
+ * branch).
9
+ *
10
+ * The original branch flattened every environment's runs into one
11
+ * `timestamp DESC` list and handed the interleaved list to the threshold
12
+ * evaluator (the default `consecutive` mode). Consecutive mode walks
13
+ * newest-first and breaks the streak on the first interleaving env, so the
14
+ * rollup collapsed to whichever env ran last in the batch — masking any
15
+ * permanently-failing sibling env ("the healthy env wins" / latest-wins)
16
+ * and flapping whenever env insertion order drifted across ticks.
17
+ *
18
+ * The fix evaluates the threshold window PER ENVIRONMENT within the
19
+ * association and takes worst-wins across envs (unhealthy > degraded >
20
+ * healthy), making the rollup stable regardless of insertion order or
21
+ * multi-pod racing. These tests pin that behavior with a mocked DB that
22
+ * returns the interleaved mixed-pool the real query would surface.
23
+ */
24
+ describe("HealthCheckService - system rollup worst-wins across environments", () => {
25
+ /**
26
+ * The mixed-pool query captured by the mock. Ordered DESC (newest first),
27
+ * exactly the shape the real `health_check_runs` query returns. Two envs
28
+ * (`prod`, `staging`) of one assignment, both fanning out every tick, prod
29
+ * permanently unhealthy and staging permanently healthy. The env insertion
30
+ * order in the executor is sequential membership order, so prod lands before
31
+ * staging, making the latest run in the pool a staging-healthy run — the
32
+ * exact scenario that masked prod's outage under flattening.
33
+ */
34
+ const PROD_RUN = { status: "unhealthy" as const, environmentId: "prod" };
35
+ const STAGE_RUN = { status: "healthy" as const, environmentId: "staging" };
36
+
37
+ function buildMixedPool(ticksPerEnv = 5): { status: "unhealthy" | "healthy"; timestamp: Date; environmentId: string }[] {
38
+ const pool: { status: "unhealthy" | "healthy"; timestamp: Date; environmentId: string }[] = [];
39
+ for (let i = 0; i < ticksPerEnv; i++) {
40
+ pool.push({ ...PROD_RUN, timestamp: new Date(2025, 0, 1, 0, 0, i) });
41
+ pool.push({ ...STAGE_RUN, timestamp: new Date(2025, 0, 1, 0, 0, i + 0.5) });
42
+ }
43
+ return pool; // DESC at the DB layer; we return newest-first below.
44
+ }
45
+
46
+ function createMockDb(runsMixedDesc: { status: string; timestamp: Date; environmentId: string }[]) {
47
+ const assocWhere = mock(() => Promise.resolve([
48
+ {
49
+ configurationId: "config-1",
50
+ configName: "HTTP probe",
51
+ enabled: true,
52
+ paused: false,
53
+ stateThresholds: null,
54
+ },
55
+ ]));
56
+ const assocInnerJoin = Object.assign(Promise.resolve([]), { where: assocWhere });
57
+ const assocFrom = Object.assign(Promise.resolve([]), { innerJoin: mock(() => assocInnerJoin) });
58
+
59
+ const runsLimit = mock(() => Promise.resolve(runsMixedDesc));
60
+ const runsOrderBy = mock(() => ({ limit: runsLimit }));
61
+ const runsWhere = mock(() => ({ orderBy: runsOrderBy, limit: runsLimit }));
62
+ const runsFrom = Object.assign(Promise.resolve(runsMixedDesc), {
63
+ where: runsWhere,
64
+ orderBy: runsOrderBy,
65
+ });
66
+
67
+ let selectCallCount = 0;
68
+ return {
69
+ select: mock(() => {
70
+ selectCallCount += 1;
71
+ if (selectCallCount === 1) return { from: mock(() => assocFrom) };
72
+ return { from: mock(() => runsFrom) };
73
+ }),
74
+ insert: mock(() => ({
75
+ values: mock(() => ({
76
+ onConflictDoUpdate: mock(() => Promise.resolve()),
77
+ onConflictDoNothing: mock(() => Promise.resolve()),
78
+ returning: mock(() => Promise.resolve([])),
79
+ })),
80
+ })),
81
+ update: mock(() => ({ set: mock(() => ({ where: mock(() => Promise.resolve()) })) })),
82
+ delete: mock(() => ({ where: mock(() => Promise.resolve()) })),
83
+ execute: mock(() => Promise.resolve()),
84
+ };
85
+ }
86
+
87
+ it("the rollup reports unhealthy when ONE env is permanently unhealthy, the other healthy", async () => {
88
+ // DB returns newest-first interleaved runs. The pre-fix behavior would
89
+ // mask prod's outage because the latest run is staging-healthy; the
90
+ // threshold evaluator (default consecutive mode) walks newest-first from
91
+ // staging-healthy, breaks the streak on the very next prod-unhealthy run,
92
+ // and falls back to `"healthy"`.
93
+ const pool = buildMixedPool(5);
94
+ const runsDesc = pool.toReversed(); // oldest produced first above; reverse to DESC
95
+ const mockDb = createMockDb(runsDesc as never);
96
+ const service = new HealthCheckService(mockDb as never, {} as never, {} as never);
97
+
98
+ const result = await service.getSystemHealthStatus("system-1");
99
+
100
+ expect(result.status).toBe("unhealthy");
101
+ expect(result.checkStatuses).toHaveLength(1);
102
+ expect(result.checkStatuses[0].status).toBe("unhealthy");
103
+ expect(result.checkStatuses[0].runsConsidered).toBe(pool.length);
104
+ });
105
+
106
+ it("flattening the same mixed pool through the evaluator (the pre-fix derivation) would have returned `healthy`", async () => {
107
+ // Sanity check: the very data the rollup branch reads, fed directly to
108
+ // `evaluateHealthStatus` as one flat interleaved list, collapses to
109
+ // "healthy" — the precise regression this fix replaces with per-env
110
+ // evaluation. Pinning it here guards against a relax of the test above.
111
+ const pool = buildMixedPool(5);
112
+ const runsDesc = pool.toReversed();
113
+ const flatStatus = evaluateHealthStatus({
114
+ runs: runsDesc as never,
115
+ });
116
+ expect(flatStatus).toBe("healthy");
117
+ });
118
+
119
+ it("reports healthy only when EVERY env is healthy", async () => {
120
+ const allHealthy = Array.from({ length: 10 }, (_, i) => ({
121
+ status: "healthy",
122
+ timestamp: new Date(2025, 0, 1, 0, 0, i),
123
+ environmentId: i % 2 === 0 ? "prod" : "staging",
124
+ })).toReversed();
125
+ const mockDb = createMockDb(allHealthy as never);
126
+ const service = new HealthCheckService(mockDb as never, {} as never, {} as never);
127
+
128
+ const result = await service.getSystemHealthStatus("system-1");
129
+ expect(result.status).toBe("healthy");
130
+ });
131
+
132
+ it("degrades (not flaps) when one env is degraded and the other healthy", async () => {
133
+ // Default consecutive thresholds need 2 consecutive failures to escalate
134
+ // to `degraded` (and 5 to escalate to `unhealthy` — so keep prod's streak
135
+ // at exactly 2 degraded runs). Per-env: prod's env-sorted slice =
136
+ // [degraded, degraded] (newest first) → degraded; staging → healthy.
137
+ // Rollup worst-wins = degraded. Flattening would break on the staging
138
+ // interleave and return `healthy` (the masked bug); per-env gives a
139
+ // stable `degraded`.
140
+ const pool: { status: "healthy" | "degraded"; timestamp: Date; environmentId: string }[] = [];
141
+ for (let i = 0; i < 2; i++) {
142
+ pool.push({ status: "degraded", timestamp: new Date(2025, 0, 1, 0, 0, i), environmentId: "prod" });
143
+ pool.push({ status: "healthy", timestamp: new Date(2025, 0, 1, 0, 0, i + 0.5), environmentId: "staging" });
144
+ }
145
+ const runsDesc = pool.toReversed();
146
+ const mockDb = createMockDb(runsDesc as never);
147
+ const service = new HealthCheckService(mockDb as never, {} as never, {} as never);
148
+
149
+ const result = await service.getSystemHealthStatus("system-1");
150
+ expect(result.status).toBe("degraded");
151
+ });
152
+
153
+ it("the per-env slice (concrete environmentId) is unaffected — only the rollup branch changed", async () => {
154
+ // Pass an explicit environmentId; the old per-env branch (string envId)
155
+ // must continue to filter to that env's slice. Here we ask for `prod`
156
+ // and expect unhealthy.
157
+ const prodOnly = Array.from({ length: 5 }, (_, i) => ({
158
+ status: "unhealthy",
159
+ timestamp: new Date(2025, 0, 1, 0, 0, i),
160
+ }));
161
+ // Mock: the runs query mirrors the predicate back to prodOnly.
162
+ const assocWhere = mock(() => Promise.resolve([
163
+ {
164
+ configurationId: "config-1",
165
+ configName: "HTTP probe",
166
+ enabled: true,
167
+ paused: false,
168
+ stateThresholds: null,
169
+ },
170
+ ]));
171
+ const assocInnerJoin = Object.assign(Promise.resolve([]), { where: assocWhere });
172
+ const assocFrom = Object.assign(Promise.resolve([]), { innerJoin: mock(() => assocInnerJoin) });
173
+
174
+ const runsLimit = mock(() => Promise.resolve(prodOnly));
175
+ const runsOrderBy = mock(() => ({ limit: runsLimit }));
176
+ const runsWhere = mock(() => ({ orderBy: runsOrderBy, limit: runsLimit }));
177
+ const runsFrom = Object.assign(Promise.resolve(prodOnly), {
178
+ where: runsWhere,
179
+ orderBy: runsOrderBy,
180
+ });
181
+
182
+ let selectCallCount = 0;
183
+ const mockDb = {
184
+ select: mock(() => {
185
+ selectCallCount += 1;
186
+ if (selectCallCount === 1) return { from: mock(() => assocFrom) };
187
+ return { from: mock(() => runsFrom) };
188
+ }),
189
+ insert: mock(() => ({
190
+ values: mock(() => ({
191
+ onConflictDoUpdate: mock(() => Promise.resolve()),
192
+ onConflictDoNothing: mock(() => Promise.resolve()),
193
+ returning: mock(() => Promise.resolve([])),
194
+ })),
195
+ })),
196
+ update: mock(() => ({ set: mock(() => ({ where: mock(() => Promise.resolve()) })) })),
197
+ delete: mock(() => ({ where: mock(() => Promise.resolve()) })),
198
+ execute: mock(() => Promise.resolve()),
199
+ };
200
+
201
+ const service = new HealthCheckService(mockDb as never, {} as never, {} as never);
202
+ const result = await service.getSystemHealthStatus("system-1", "prod");
203
+ expect(result.status).toBe("unhealthy");
204
+ });
205
+ });