@checkstack/healthcheck-backend 1.21.3 → 1.23.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.
- package/CHANGELOG.md +314 -0
- package/drizzle/0021_amazing_wolf_cub.sql +8 -0
- package/drizzle/meta/0021_snapshot.json +717 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +31 -29
- package/src/ai/system-signals-contributor.test.ts +1 -0
- package/src/cache.test.ts +3 -0
- package/src/effective-environments.test.ts +63 -2
- package/src/effective-environments.ts +34 -0
- package/src/health-entity.ts +8 -2
- package/src/health-state.ts +15 -6
- package/src/index.ts +33 -0
- package/src/queue-executor.test.ts +270 -0
- package/src/queue-executor.ts +672 -569
- package/src/router-satellite-ingest.test.ts +136 -0
- package/src/router.ts +65 -4
- package/src/satellite-liveness.test.ts +199 -0
- package/src/satellite-liveness.ts +106 -0
- package/src/schema.ts +21 -0
- package/src/service-batching.test.ts +3 -1
- package/src/service-ingest-assertions.test.ts +33 -60
- package/src/service-paused-filter.test.ts +9 -4
- package/src/service-rollup-worst-wins.test.ts +147 -55
- package/src/service.ts +355 -148
- package/src/state-evaluator.test.ts +49 -0
- package/src/system-health-override.ts +6 -1
- package/tsconfig.json +6 -0
|
@@ -13,8 +13,9 @@ import { HealthCheckService } from "./service";
|
|
|
13
13
|
* Satellite ingest evaluates assertions ON THE CORE (satellites never held
|
|
14
14
|
* the assertion semantics — before this, satellite-executed checks silently
|
|
15
15
|
* skipped assertions), then strips ephemeral fields for parity with local
|
|
16
|
-
* runs.
|
|
17
|
-
*
|
|
16
|
+
* runs. `processSatelliteResult` returns the processed status + result record
|
|
17
|
+
* (and the check name); the shared post-run path persists it. These tests
|
|
18
|
+
* assert on that returned payload.
|
|
18
19
|
*/
|
|
19
20
|
|
|
20
21
|
const KEY = computeAssertionKey({
|
|
@@ -30,43 +31,17 @@ const collectorResultSchema = z.object({
|
|
|
30
31
|
body: healthResultString({ "x-ephemeral": true }),
|
|
31
32
|
});
|
|
32
33
|
|
|
33
|
-
function buildService({
|
|
34
|
-
|
|
35
|
-
inserted,
|
|
36
|
-
}: {
|
|
37
|
-
entries: CollectorConfigEntry[];
|
|
38
|
-
inserted: Record<string, unknown>[];
|
|
39
|
-
}) {
|
|
40
|
-
const tx = {
|
|
41
|
-
insert: mock(() => ({
|
|
42
|
-
values: mock((vals: Record<string, unknown>) => {
|
|
43
|
-
inserted.push(vals);
|
|
44
|
-
return Object.assign(Promise.resolve(), {
|
|
45
|
-
onConflictDoUpdate: mock(() => Promise.resolve()),
|
|
46
|
-
onConflictDoNothing: mock(() => Promise.resolve()),
|
|
47
|
-
});
|
|
48
|
-
}),
|
|
49
|
-
})),
|
|
34
|
+
function buildService({ entries }: { entries: CollectorConfigEntry[] }) {
|
|
35
|
+
const db = {
|
|
50
36
|
select: mock(() => ({
|
|
51
37
|
from: mock(() => ({
|
|
52
38
|
where: mock(() =>
|
|
53
|
-
|
|
54
|
-
limit: mock(() => Promise.resolve([])),
|
|
55
|
-
}),
|
|
39
|
+
Promise.resolve([{ name: "Test check", collectors: entries }]),
|
|
56
40
|
),
|
|
57
41
|
})),
|
|
58
42
|
})),
|
|
59
43
|
};
|
|
60
44
|
|
|
61
|
-
const db = {
|
|
62
|
-
select: mock(() => ({
|
|
63
|
-
from: mock(() => ({
|
|
64
|
-
where: mock(() => Promise.resolve([{ collectors: entries }])),
|
|
65
|
-
})),
|
|
66
|
-
})),
|
|
67
|
-
transaction: mock(async (fn: (t: typeof tx) => Promise<void>) => fn(tx)),
|
|
68
|
-
};
|
|
69
|
-
|
|
70
45
|
const collectorRegistry = {
|
|
71
46
|
register: mock(() => {}),
|
|
72
47
|
getCollector: mock(() => ({
|
|
@@ -113,64 +88,59 @@ const entries: CollectorConfigEntry[] = [
|
|
|
113
88
|
},
|
|
114
89
|
];
|
|
115
90
|
|
|
116
|
-
async function
|
|
91
|
+
async function process({
|
|
117
92
|
entries,
|
|
118
93
|
statusCode,
|
|
119
94
|
}: {
|
|
120
95
|
entries: CollectorConfigEntry[];
|
|
121
96
|
statusCode: number;
|
|
122
97
|
}) {
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
await service.ingestSatelliteResult({
|
|
98
|
+
const service = buildService({ entries });
|
|
99
|
+
return service.processSatelliteResult({
|
|
126
100
|
configId: "config-1",
|
|
127
|
-
systemId: "system-1",
|
|
128
101
|
status: "healthy",
|
|
129
|
-
latencyMs: 42,
|
|
130
102
|
result: satelliteResult({ statusCode }) as never,
|
|
131
|
-
executedAt: "2026-07-03T10:00:00.000Z",
|
|
132
|
-
sourceId: "sat-1",
|
|
133
|
-
sourceLabel: "EU West",
|
|
134
103
|
});
|
|
135
|
-
const runInsert = inserted.find((v) => "status" in v && "result" in v);
|
|
136
|
-
expect(runInsert).toBeDefined();
|
|
137
|
-
return runInsert as Record<string, unknown>;
|
|
138
104
|
}
|
|
139
105
|
|
|
140
|
-
function collectorEntryOf(
|
|
141
|
-
const result =
|
|
106
|
+
function collectorEntryOf(resultRecord: Record<string, unknown>) {
|
|
107
|
+
const result = resultRecord as {
|
|
142
108
|
metadata: { collectors: Record<string, Record<string, unknown>> };
|
|
143
109
|
};
|
|
144
110
|
return result.metadata.collectors["entry-1"];
|
|
145
111
|
}
|
|
146
112
|
|
|
147
|
-
describe("
|
|
113
|
+
describe("processSatelliteResult - assertion evaluation at ingest", () => {
|
|
148
114
|
it("downgrades a satellite-healthy run whose assertion fails", async () => {
|
|
149
|
-
const
|
|
150
|
-
expect(
|
|
115
|
+
const { status, resultRecord } = await process({ entries, statusCode: 404 });
|
|
116
|
+
expect(status).toBe("unhealthy");
|
|
151
117
|
|
|
152
|
-
const entry = collectorEntryOf(
|
|
118
|
+
const entry = collectorEntryOf(resultRecord);
|
|
153
119
|
expect(entry._assertionFailed).toBe("statusCode equals 200");
|
|
154
120
|
expect(entry._assertions).toEqual([
|
|
155
121
|
expect.objectContaining({ key: KEY, passed: false, actual: "404" }),
|
|
156
122
|
]);
|
|
157
|
-
|
|
158
|
-
expect(message).toBe(
|
|
123
|
+
expect((resultRecord as { message: string }).message).toBe(
|
|
159
124
|
"Check failed: Assertion failed: statusCode equals 200",
|
|
160
125
|
);
|
|
161
126
|
});
|
|
162
127
|
|
|
163
128
|
it("keeps a passing run healthy and stores the passing outcome", async () => {
|
|
164
|
-
const
|
|
165
|
-
expect(
|
|
129
|
+
const { status, resultRecord } = await process({ entries, statusCode: 200 });
|
|
130
|
+
expect(status).toBe("healthy");
|
|
166
131
|
|
|
167
|
-
const entry = collectorEntryOf(
|
|
132
|
+
const entry = collectorEntryOf(resultRecord);
|
|
168
133
|
expect(entry._assertionFailed).toBeUndefined();
|
|
169
134
|
expect(entry._assertions).toEqual([
|
|
170
135
|
expect.objectContaining({ key: KEY, passed: true, actual: "200" }),
|
|
171
136
|
]);
|
|
172
137
|
});
|
|
173
138
|
|
|
139
|
+
it("resolves the check name for the notification", async () => {
|
|
140
|
+
const { configName } = await process({ entries, statusCode: 200 });
|
|
141
|
+
expect(configName).toBe("Test check");
|
|
142
|
+
});
|
|
143
|
+
|
|
174
144
|
it("strips ephemeral fields AFTER assertions ran against them", async () => {
|
|
175
145
|
const withBodyAssertion: CollectorConfigEntry[] = [
|
|
176
146
|
{
|
|
@@ -187,12 +157,12 @@ describe("ingestSatelliteResult - assertion evaluation at ingest", () => {
|
|
|
187
157
|
],
|
|
188
158
|
},
|
|
189
159
|
];
|
|
190
|
-
const
|
|
160
|
+
const { status, resultRecord } = await process({
|
|
191
161
|
entries: withBodyAssertion,
|
|
192
162
|
statusCode: 200,
|
|
193
163
|
});
|
|
194
164
|
|
|
195
|
-
const entry = collectorEntryOf(
|
|
165
|
+
const entry = collectorEntryOf(resultRecord);
|
|
196
166
|
// The JSONPath assertion evaluated against the (ephemeral) body...
|
|
197
167
|
expect(entry._assertions).toEqual([
|
|
198
168
|
expect.objectContaining({ passed: true, actual: "ok" }),
|
|
@@ -200,14 +170,17 @@ describe("ingestSatelliteResult - assertion evaluation at ingest", () => {
|
|
|
200
170
|
// ...but the body itself never reaches storage.
|
|
201
171
|
expect(entry.body).toBeUndefined();
|
|
202
172
|
expect(entry.statusCode).toBe(200);
|
|
203
|
-
expect(
|
|
173
|
+
expect(status).toBe("healthy");
|
|
204
174
|
});
|
|
205
175
|
|
|
206
176
|
it("tolerates collector entries the config no longer knows", async () => {
|
|
207
|
-
const
|
|
177
|
+
const { status, resultRecord } = await process({
|
|
178
|
+
entries: [],
|
|
179
|
+
statusCode: 500,
|
|
180
|
+
});
|
|
208
181
|
// No assertions configured: status passes through untouched.
|
|
209
|
-
expect(
|
|
210
|
-
const entry = collectorEntryOf(
|
|
182
|
+
expect(status).toBe("healthy");
|
|
183
|
+
const entry = collectorEntryOf(resultRecord);
|
|
211
184
|
expect(entry._assertions).toBeUndefined();
|
|
212
185
|
});
|
|
213
186
|
});
|
|
@@ -159,9 +159,12 @@ describe("HealthCheckService - paused configuration filtering", () => {
|
|
|
159
159
|
const result = await service.getSystemHealthStatus("system-1");
|
|
160
160
|
|
|
161
161
|
// The post-filter associations list is empty, so the system has no
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
|
|
162
|
+
// ACTIVE checks. Paused failures still do NOT keep the system degraded -
|
|
163
|
+
// that is the behaviour this test guards - but the result is now
|
|
164
|
+
// `unknown` rather than `healthy`: with its only check paused, nothing is
|
|
165
|
+
// measuring this system, and claiming health it has no evidence for is
|
|
166
|
+
// what made a broken check read green on the catalog and status page.
|
|
167
|
+
expect(result.status).toBe("unknown");
|
|
165
168
|
expect(result.checkStatuses).toHaveLength(0);
|
|
166
169
|
});
|
|
167
170
|
|
|
@@ -260,7 +263,9 @@ describe("HealthCheckService - paused configuration filtering", () => {
|
|
|
260
263
|
|
|
261
264
|
const result = await service.getSystemHealthStatus("system-1");
|
|
262
265
|
|
|
263
|
-
|
|
266
|
+
// No enabled associations = nothing measured, so `unknown` rather than an
|
|
267
|
+
// invented `healthy`.
|
|
268
|
+
expect(result.status).toBe("unknown");
|
|
264
269
|
expect(result.checkStatuses).toHaveLength(0);
|
|
265
270
|
});
|
|
266
271
|
});
|
|
@@ -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)
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
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 {
|
|
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
|
-
|
|
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-
|
|
85
|
-
const
|
|
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
|
|
88
|
-
|
|
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 =
|
|
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
|
|
135
|
+
// Distinct slice keys query: selectDistinct({environmentId, sourceId}).
|
|
100
136
|
const distinctFrom = Object.assign(Promise.resolve([]), {
|
|
101
137
|
where: mock(() =>
|
|
102
|
-
Promise.resolve(
|
|
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("
|
|
249
|
-
//
|
|
250
|
-
//
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
});
|