@checkstack/healthcheck-backend 1.21.2 → 1.22.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.
@@ -48,18 +48,46 @@ type Run = { status: "healthy" | "degraded" | "unhealthy"; timestamp: Date };
48
48
 
49
49
  /**
50
50
  * Build a mock db for the rollup path. `runsByEnv` maps each environment key
51
- * (`null` = env-less) to that env's runs (DESC). `environmentIds` is the
52
- * assignment's selector under test. The per-env runs query resolves against the
53
- * concrete env id bound in its predicate (or the env-less slice when none of the
54
- * known env ids appear, i.e. the `isNull` clause).
51
+ * (`null` = env-less) to that env's runs (DESC), all attributed to the LOCAL
52
+ * core; `satelliteRuns` adds slices probed from a satellite. `environmentIds` is
53
+ * the assignment's selector under test.
54
+ *
55
+ * The per-slice runs query resolves against the env id AND source id bound in
56
+ * its predicate. Known env ids and satellite ids are disjoint in these fixtures,
57
+ * so each is recovered by set membership; an absent value means the `isNull`
58
+ * clause, i.e. the env-less / local slice.
55
59
  */
56
60
  function createRollupMockDb(props: {
57
61
  runsByEnv: Map<string | null, Run[]>;
58
62
  environmentIds: string[] | null;
63
+ satelliteRuns?: { sourceId: string; environmentId: string | null; runs: Run[] }[];
64
+ satelliteIds?: string[] | null;
65
+ satelliteEnvironmentIds?: Record<string, string[] | null> | null;
66
+ includeLocal?: boolean;
59
67
  }) {
60
- const { runsByEnv, environmentIds } = props;
68
+ const {
69
+ runsByEnv,
70
+ environmentIds,
71
+ satelliteRuns = [],
72
+ satelliteIds = null,
73
+ satelliteEnvironmentIds = null,
74
+ includeLocal = true,
75
+ } = props;
76
+ const slices = [
77
+ ...[...runsByEnv].map(([environmentId, runs]) => ({
78
+ environmentId,
79
+ sourceId: null as string | null,
80
+ runs,
81
+ })),
82
+ ...satelliteRuns,
83
+ ];
61
84
  const knownEnvIds = new Set(
62
- [...runsByEnv.keys()].filter((k): k is string => k !== null),
85
+ slices
86
+ .map((s) => s.environmentId)
87
+ .filter((k): k is string => k !== null),
88
+ );
89
+ const knownSourceIds = new Set(
90
+ slices.map((s) => s.sourceId).filter((k): k is string => k !== null),
63
91
  );
64
92
 
65
93
  const assocWhere = mock(() =>
@@ -71,6 +99,9 @@ function createRollupMockDb(props: {
71
99
  paused: false,
72
100
  stateThresholds: null,
73
101
  environmentIds,
102
+ satelliteIds,
103
+ satelliteEnvironmentIds,
104
+ includeLocal,
74
105
  },
75
106
  ]),
76
107
  );
@@ -81,25 +112,35 @@ function createRollupMockDb(props: {
81
112
  innerJoin: mock(() => assocInnerJoin),
82
113
  });
83
114
 
84
- // Per-env runs query: pick the slice named by the predicate's env value.
85
- const resolvePerEnv = (predicate: unknown): Run[] => {
115
+ // Per-slice runs query: pick the slice named by the predicate's env + source.
116
+ const resolveSlice = (predicate: unknown): Run[] => {
86
117
  const values = collectPredicateValues(predicate);
87
- const envId = values.find((v) => knownEnvIds.has(v)) ?? null;
88
- return runsByEnv.get(envId) ?? [];
118
+ const environmentId = values.find((v) => knownEnvIds.has(v)) ?? null;
119
+ const sourceId = values.find((v) => knownSourceIds.has(v)) ?? null;
120
+ return (
121
+ slices.find(
122
+ (s) => s.environmentId === environmentId && s.sourceId === sourceId,
123
+ )?.runs ?? []
124
+ );
89
125
  };
90
126
  const runsFromFor = () => {
91
127
  const runsWhere = mock((predicate: unknown) => {
92
- const rows = resolvePerEnv(predicate);
128
+ const rows = resolveSlice(predicate);
93
129
  const limit = mock(() => Promise.resolve(rows));
94
130
  return { orderBy: mock(() => ({ limit })), limit };
95
131
  });
96
132
  return Object.assign(Promise.resolve([]), { where: runsWhere });
97
133
  };
98
134
 
99
- // Distinct env keys query: select({environmentId}).from().where().
135
+ // Distinct slice keys query: selectDistinct({environmentId, sourceId}).
100
136
  const distinctFrom = Object.assign(Promise.resolve([]), {
101
137
  where: mock(() =>
102
- Promise.resolve([...runsByEnv.keys()].map((k) => ({ environmentId: k }))),
138
+ Promise.resolve(
139
+ slices.map(({ environmentId, sourceId }) => ({
140
+ environmentId,
141
+ sourceId,
142
+ })),
143
+ ),
103
144
  ),
104
145
  });
105
146
 
@@ -245,58 +286,109 @@ describe("HealthCheckService - system rollup worst-wins across environments", ()
245
286
  expect(result.checkStatuses[0].runsConsidered).toBe(3);
246
287
  });
247
288
 
248
- it("the per-env slice (concrete environmentId) is unaffected — only the rollup branch changed", async () => {
249
- // Pass an explicit environmentId; the per-env branch (string envId) still
250
- // filters to that env's slice via a single windowed query and reads unhealthy.
251
- const prodOnly = runs("unhealthy", 5);
252
- const assocWhere = mock(() =>
253
- Promise.resolve([
254
- {
255
- configurationId: "config-1",
256
- configName: "HTTP probe",
257
- enabled: true,
258
- paused: false,
259
- stateThresholds: null,
260
- environmentIds: null,
261
- },
262
- ]),
263
- );
264
- const assocInnerJoin = Object.assign(Promise.resolve([]), { where: assocWhere });
265
- const assocFrom = Object.assign(Promise.resolve([]), {
266
- innerJoin: mock(() => assocInnerJoin),
289
+ it("reports unhealthy when the LOCAL check passes but a SATELLITE check fails", async () => {
290
+ // The reported bug (@stuajnht): a system read HEALTHY while one of its
291
+ // probe locations failed every single time. Both sources' runs landed in
292
+ // one slice, so `evaluateConsecutive` saw healthy/unhealthy alternating,
293
+ // broke its streak on every run, met no threshold, and fell through to its
294
+ // healthy default. Sliced per source, the satellite is evaluated on its own
295
+ // and worst-wins carries it to the system.
296
+ const runsByEnv = new Map<string | null, Run[]>([[null, runs("healthy", 5)]]);
297
+ const mockDb = createRollupMockDb({
298
+ runsByEnv,
299
+ environmentIds: null,
300
+ satelliteIds: ["sat-eu"],
301
+ satelliteRuns: [
302
+ { sourceId: "sat-eu", environmentId: null, runs: runs("unhealthy", 5) },
303
+ ],
267
304
  });
305
+ const service = new HealthCheckService(mockDb as never, {} as never, {} as never);
306
+
307
+ const result = await service.getSystemHealthStatus("system-1");
268
308
 
269
- const runsLimit = mock(() => Promise.resolve(prodOnly));
270
- const runsOrderBy = mock(() => ({ limit: runsLimit }));
271
- const runsWhere = mock(() => ({ orderBy: runsOrderBy, limit: runsLimit }));
272
- const runsFrom = Object.assign(Promise.resolve(prodOnly), {
273
- where: runsWhere,
274
- orderBy: runsOrderBy,
309
+ expect(result.status).toBe("unhealthy");
310
+ expect(result.checkStatuses[0].status).toBe("unhealthy");
311
+ // Two locations probing one environment = two slices, one failing.
312
+ expect(result.checkStatuses[0].sliceCount).toBe(2);
313
+ expect(result.checkStatuses[0].failingSliceCount).toBe(1);
314
+ });
315
+
316
+ it("names the failing location in the per-slice breakdown", async () => {
317
+ const runsByEnv = new Map<string | null, Run[]>([["prod", runs("healthy", 5)]]);
318
+ const mockDb = createRollupMockDb({
319
+ runsByEnv,
320
+ environmentIds: ["prod"],
321
+ satelliteIds: ["sat-eu"],
322
+ satelliteRuns: [
323
+ { sourceId: "sat-eu", environmentId: "prod", runs: runs("unhealthy", 5) },
324
+ ],
275
325
  });
326
+ const service = new HealthCheckService(mockDb as never, {} as never, {} as never);
276
327
 
277
- let selectCallCount = 0;
278
- const mockDb = withTransactionMock({
279
- select: mock(() => {
280
- selectCallCount += 1;
281
- if (selectCallCount === 1) return { from: mock(() => assocFrom) };
282
- return { from: mock(() => runsFrom) };
283
- }),
284
- insert: mock(() => ({
285
- values: mock(() => ({
286
- onConflictDoUpdate: mock(() => Promise.resolve()),
287
- onConflictDoNothing: mock(() => Promise.resolve()),
288
- returning: mock(() => Promise.resolve([])),
289
- })),
290
- })),
291
- update: mock(() => ({ set: mock(() => ({ where: mock(() => Promise.resolve()) })) })),
292
- delete: mock(() => ({ where: mock(() => Promise.resolve()) })),
293
- execute: mock(() => Promise.resolve()),
328
+ const { slices } = (await service.getSystemHealthStatus("system-1")).checkStatuses[0];
329
+
330
+ expect(slices).toHaveLength(2);
331
+ expect(slices.find((s) => s.sourceId === null)?.status).toBe("healthy");
332
+ expect(slices.find((s) => s.sourceId === "sat-eu")?.status).toBe("unhealthy");
333
+ });
334
+
335
+ it("drops a de-assigned satellite's stale failing runs", async () => {
336
+ // No health-change event fires for a slice that merely stopped producing
337
+ // runs, so without the effective-source filter an unassigned satellite's
338
+ // last failures would drag the rollup until they aged out of the window.
339
+ const runsByEnv = new Map<string | null, Run[]>([[null, runs("healthy", 5)]]);
340
+ const mockDb = createRollupMockDb({
341
+ runsByEnv,
342
+ environmentIds: null,
343
+ satelliteIds: [],
344
+ satelliteRuns: [
345
+ { sourceId: "sat-gone", environmentId: null, runs: runs("unhealthy", 5) },
346
+ ],
294
347
  });
348
+ const service = new HealthCheckService(mockDb as never, {} as never, {} as never);
349
+
350
+ const result = await service.getSystemHealthStatus("system-1");
351
+
352
+ expect(result.status).toBe("healthy");
353
+ expect(result.checkStatuses[0].sliceCount).toBe(1);
354
+ });
355
+
356
+ it("keeps evaluating satellites when the core stopped running the check", async () => {
357
+ // `includeLocal: false` means the core's old runs no longer contribute, so
358
+ // the satellite alone decides.
359
+ const runsByEnv = new Map<string | null, Run[]>([[null, runs("healthy", 5)]]);
360
+ const mockDb = createRollupMockDb({
361
+ runsByEnv,
362
+ environmentIds: null,
363
+ includeLocal: false,
364
+ satelliteIds: ["sat-eu"],
365
+ satelliteRuns: [
366
+ { sourceId: "sat-eu", environmentId: null, runs: runs("unhealthy", 5) },
367
+ ],
368
+ });
369
+ const service = new HealthCheckService(mockDb as never, {} as never, {} as never);
370
+
371
+ const result = await service.getSystemHealthStatus("system-1");
295
372
 
373
+ expect(result.status).toBe("unhealthy");
374
+ expect(result.checkStatuses[0].sliceCount).toBe(1);
375
+ expect(result.checkStatuses[0].failingSliceCount).toBe(1);
376
+ });
377
+
378
+ it("a pinned environment evaluates only that environment's slices", async () => {
379
+ // Passing an explicit environmentId narrows to that env - the other env's
380
+ // runs must not contribute - while the source dimension is still sliced.
381
+ const runsByEnv = new Map<string | null, Run[]>([
382
+ ["prod", runs("unhealthy", 5)],
383
+ ["staging", runs("healthy", 5)],
384
+ ]);
385
+ const mockDb = createRollupMockDb({ runsByEnv, environmentIds: null });
296
386
  const service = new HealthCheckService(mockDb as never, {} as never, {} as never);
387
+
297
388
  const result = await service.getSystemHealthStatus("system-1", "prod");
298
389
  expect(result.status).toBe("unhealthy");
299
390
  expect(result.checkStatuses[0].sliceCount).toBe(1);
300
391
  expect(result.checkStatuses[0].failingSliceCount).toBe(1);
392
+ expect(result.checkStatuses[0].runsConsidered).toBe(5);
301
393
  });
302
394
  });