@checkstack/healthcheck-backend 1.17.0 → 1.19.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.
Files changed (44) hide show
  1. package/CHANGELOG.md +559 -0
  2. package/package.json +32 -29
  3. package/src/adaptive-timeout.test.ts +91 -0
  4. package/src/adaptive-timeout.ts +75 -0
  5. package/src/ai/system-signals-contributor.test.ts +2 -0
  6. package/src/automations.test.ts +47 -0
  7. package/src/automations.ts +19 -3
  8. package/src/health-notification-content.test.ts +89 -0
  9. package/src/health-notification-content.ts +138 -0
  10. package/src/healthcheck-gitops-kinds.test.ts +34 -2
  11. package/src/healthcheck-gitops-kinds.ts +17 -13
  12. package/src/index.ts +58 -6
  13. package/src/migration-chain-contract.test.ts +7 -1
  14. package/src/notification-policy.test.ts +19 -0
  15. package/src/notification-policy.ts +26 -0
  16. package/src/queue-executor.test.ts +391 -338
  17. package/src/queue-executor.ts +426 -362
  18. package/src/realtime-aggregation.ts +9 -2
  19. package/src/rollup-consumer.test.ts +191 -0
  20. package/src/rollup-consumer.ts +160 -0
  21. package/src/router.ts +46 -13
  22. package/src/schedule-jitter.test.ts +69 -0
  23. package/src/schedule-jitter.ts +50 -0
  24. package/src/schedule-reconciler.it.test.ts +453 -0
  25. package/src/schedule-reconciler.test.ts +418 -0
  26. package/src/schedule-reconciler.ts +304 -0
  27. package/src/service-batching.test.ts +106 -0
  28. package/src/service-bulk-counts.it.test.ts +144 -0
  29. package/src/service-bulk-run-stats.it.test.ts +197 -0
  30. package/src/service-ordering.test.ts +10 -2
  31. package/src/service-paused-filter.test.ts +27 -7
  32. package/src/service-rollup-worst-wins.test.ts +221 -124
  33. package/src/service.ts +557 -266
  34. package/src/slow-check-admission.test.ts +184 -0
  35. package/src/slow-check-admission.ts +101 -0
  36. package/src/slow-check-classifier.test.ts +155 -0
  37. package/src/slow-check-classifier.ts +137 -0
  38. package/src/slow-check-config.ts +102 -0
  39. package/src/status-page/rollup.test.ts +40 -0
  40. package/src/status-page/rollup.ts +27 -0
  41. package/src/status-page/widgets.test.ts +303 -0
  42. package/src/status-page/widgets.ts +155 -39
  43. package/src/suspect-lane.test.ts +50 -0
  44. package/src/suspect-lane.ts +61 -0
@@ -0,0 +1,106 @@
1
+ import { describe, it, expect, mock } from "bun:test";
2
+ import { withTransactionMock } from "@checkstack/test-utils-backend";
3
+ import { HealthCheckService } from "./service";
4
+
5
+ /**
6
+ * Regression: `getSystemHealthStatus` batches its 1 (associations) + N
7
+ * (per-check run window) reads into ONE scoped transaction (see
8
+ * `withScopedTransaction`), so the whole read fan-out pays a single
9
+ * BEGIN/SET LOCAL/COMMIT and holds one connection instead of 1+N standalone
10
+ * scoped queries. This pins that the reads run inside `db.transaction(...)`
11
+ * (exactly once per call), independent of how many checks a system has.
12
+ */
13
+ describe("HealthCheckService.getSystemHealthStatus - read batching", () => {
14
+ function createMockDb(configCount: number) {
15
+ const associations = Array.from({ length: configCount }, (_, i) => ({
16
+ configurationId: `config-${i}`,
17
+ configName: `Check ${i}`,
18
+ enabled: true,
19
+ paused: false,
20
+ stateThresholds: null,
21
+ // All-environments selector; each check has only the env-less slice below.
22
+ environmentIds: null,
23
+ }));
24
+ const assocWhere = mock(() => Promise.resolve(associations));
25
+ const assocInnerJoin = Object.assign(Promise.resolve([]), {
26
+ where: assocWhere,
27
+ });
28
+ const assocFrom = Object.assign(Promise.resolve([]), {
29
+ innerJoin: mock(() => assocInnerJoin),
30
+ });
31
+
32
+ // Each per-check run window returns one healthy run.
33
+ const healthyRun = [
34
+ { status: "healthy" as const, timestamp: new Date(), environmentId: null },
35
+ ];
36
+ const runsLimit = mock(() => Promise.resolve(healthyRun));
37
+ const runsOrderBy = mock(() => ({ limit: runsLimit }));
38
+ const runsWhere = mock(() => ({ orderBy: runsOrderBy, limit: runsLimit }));
39
+ const runsFrom = Object.assign(Promise.resolve(healthyRun), {
40
+ where: runsWhere,
41
+ orderBy: runsOrderBy,
42
+ });
43
+
44
+ // Distinct env keys query per check: a single env-less (null) slice.
45
+ const distinctFrom = Object.assign(Promise.resolve([]), {
46
+ where: mock(() => Promise.resolve([{ environmentId: null }])),
47
+ });
48
+
49
+ let selectCallCount = 0;
50
+ const db = withTransactionMock({
51
+ select: mock(() => {
52
+ selectCallCount += 1;
53
+ if (selectCallCount === 1) return { from: mock(() => assocFrom) };
54
+ return { from: mock(() => runsFrom) };
55
+ }),
56
+ selectDistinct: mock(() => ({ from: mock(() => distinctFrom) })),
57
+ insert: mock(() => ({ values: mock(() => Promise.resolve()) })),
58
+ update: mock(() => ({
59
+ set: mock(() => ({ where: mock(() => Promise.resolve()) })),
60
+ })),
61
+ delete: mock(() => ({ where: mock(() => Promise.resolve()) })),
62
+ execute: mock(() => Promise.resolve()),
63
+ });
64
+ return db;
65
+ }
66
+
67
+ it("wraps the associations + per-check reads in exactly ONE transaction", async () => {
68
+ const mockDb = createMockDb(3);
69
+ const service = new HealthCheckService(
70
+ mockDb as never,
71
+ {} as never,
72
+ {} as never,
73
+ );
74
+
75
+ const result = await service.getSystemHealthStatus("system-1");
76
+
77
+ expect(result.status).toBe("healthy");
78
+ expect(result.checkStatuses).toHaveLength(3);
79
+ // One transaction covers all 1 + N reads (not one per query).
80
+ const transaction = (mockDb as unknown as { transaction: ReturnType<typeof mock> })
81
+ .transaction;
82
+ expect(transaction).toHaveBeenCalledTimes(1);
83
+ // 1 associations select + 3 per-check run selects = 4 selects, all inside
84
+ // the single transaction.
85
+ const select = (mockDb as unknown as { select: ReturnType<typeof mock> })
86
+ .select;
87
+ expect(select).toHaveBeenCalledTimes(4);
88
+ });
89
+
90
+ it("still opens exactly one transaction for a system with no checks", async () => {
91
+ const mockDb = createMockDb(0);
92
+ const service = new HealthCheckService(
93
+ mockDb as never,
94
+ {} as never,
95
+ {} as never,
96
+ );
97
+
98
+ const result = await service.getSystemHealthStatus("system-1");
99
+
100
+ expect(result.status).toBe("healthy");
101
+ expect(result.checkStatuses).toHaveLength(0);
102
+ const transaction = (mockDb as unknown as { transaction: ReturnType<typeof mock> })
103
+ .transaction;
104
+ expect(transaction).toHaveBeenCalledTimes(1);
105
+ });
106
+ });
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Integration test for `HealthCheckService.getBulkAssignedHealthCheckCounts`
3
+ * against a REAL Postgres. The method's whole point is the GROUP BY / COUNT and
4
+ * the zero-fill for systems with no rows - behaviour a mocked db cannot prove
5
+ * (the database does the grouping). These tests pin: counts are grouped per
6
+ * system, systems with no assignments report 0, requested-but-absent systems
7
+ * report 0, and non-requested systems never leak in.
8
+ *
9
+ * Gated on CHECKSTACK_IT so it runs in CI (shared compose Postgres) and is
10
+ * skipped in the default `bun test` run, matching the other *.it.test.ts here.
11
+ */
12
+ import { afterAll, beforeAll, beforeEach, describe, expect, it } from "bun:test";
13
+ import { drizzle } from "drizzle-orm/node-postgres";
14
+ import { Pool } from "pg";
15
+ import type {
16
+ SafeDatabase,
17
+ HealthCheckRegistry,
18
+ CollectorRegistry,
19
+ } from "@checkstack/backend-api";
20
+ import * as schema from "./schema";
21
+ import { HealthCheckService } from "./service";
22
+
23
+ const PG_URL =
24
+ process.env.CHECKSTACK_IT_PG_URL ??
25
+ "postgres://postgres:postgres@localhost:5432/postgres";
26
+ const SCHEMA = "healthcheck_it_bulk_counts";
27
+
28
+ let admin: Pool;
29
+ let pool: Pool;
30
+ let service: HealthCheckService;
31
+
32
+ async function insertAssignment(row: {
33
+ systemId: string;
34
+ configurationId: string;
35
+ }): Promise<void> {
36
+ await pool.query(
37
+ `INSERT INTO "${SCHEMA}".system_health_checks (system_id, configuration_id)
38
+ VALUES ($1, $2)`,
39
+ [row.systemId, row.configurationId],
40
+ );
41
+ }
42
+
43
+ describe.skipIf(!process.env.CHECKSTACK_IT)(
44
+ "HealthCheckService.getBulkAssignedHealthCheckCounts (shared Postgres)",
45
+ () => {
46
+ beforeAll(async () => {
47
+ admin = new Pool({ connectionString: PG_URL });
48
+ await admin.query(`DROP SCHEMA IF EXISTS "${SCHEMA}" CASCADE`);
49
+ await admin.query(`CREATE SCHEMA "${SCHEMA}"`);
50
+ // Only the columns the grouped COUNT touches; no FK to configurations so
51
+ // the DDL stays minimal and focused on the aggregation behaviour.
52
+ await admin.query(
53
+ `CREATE TABLE "${SCHEMA}".system_health_checks (
54
+ system_id text NOT NULL,
55
+ configuration_id uuid NOT NULL,
56
+ PRIMARY KEY (system_id, configuration_id)
57
+ )`,
58
+ );
59
+ pool = new Pool({
60
+ connectionString: PG_URL,
61
+ options: `-c search_path=${SCHEMA}`,
62
+ });
63
+ const db = drizzle(pool, {
64
+ schema,
65
+ }) as unknown as SafeDatabase<typeof schema>;
66
+ // registry / collectorRegistry are unused by the count method under test;
67
+ // stub them so the constructor is satisfied without wiring real registries.
68
+ const service_ = new HealthCheckService(
69
+ db,
70
+ {} as unknown as HealthCheckRegistry,
71
+ {} as unknown as CollectorRegistry,
72
+ );
73
+ service = service_;
74
+ });
75
+
76
+ afterAll(async () => {
77
+ await pool?.end();
78
+ await admin.query(`DROP SCHEMA IF EXISTS "${SCHEMA}" CASCADE`);
79
+ await admin.end();
80
+ });
81
+
82
+ beforeEach(async () => {
83
+ await pool.query(`TRUNCATE "${SCHEMA}".system_health_checks`);
84
+ });
85
+
86
+ it("groups counts per system and zero-fills systems with no assignments", async () => {
87
+ // sys-a: 2 assignments, sys-b: 1, sys-c: none.
88
+ await insertAssignment({
89
+ systemId: "sys-a",
90
+ configurationId: crypto.randomUUID(),
91
+ });
92
+ await insertAssignment({
93
+ systemId: "sys-a",
94
+ configurationId: crypto.randomUUID(),
95
+ });
96
+ await insertAssignment({
97
+ systemId: "sys-b",
98
+ configurationId: crypto.randomUUID(),
99
+ });
100
+
101
+ const counts = await service.getBulkAssignedHealthCheckCounts([
102
+ "sys-a",
103
+ "sys-b",
104
+ "sys-c",
105
+ ]);
106
+
107
+ expect(counts).toEqual({ "sys-a": 2, "sys-b": 1, "sys-c": 0 });
108
+ });
109
+
110
+ it("returns 0 for a requested system that has no rows at all", async () => {
111
+ await insertAssignment({
112
+ systemId: "sys-a",
113
+ configurationId: crypto.randomUUID(),
114
+ });
115
+
116
+ const counts = await service.getBulkAssignedHealthCheckCounts([
117
+ "sys-a",
118
+ "missing",
119
+ ]);
120
+
121
+ expect(counts).toEqual({ "sys-a": 1, missing: 0 });
122
+ });
123
+
124
+ it("never leaks counts for systems not in the requested set", async () => {
125
+ await insertAssignment({
126
+ systemId: "sys-a",
127
+ configurationId: crypto.randomUUID(),
128
+ });
129
+ await insertAssignment({
130
+ systemId: "sys-other",
131
+ configurationId: crypto.randomUUID(),
132
+ });
133
+
134
+ const counts = await service.getBulkAssignedHealthCheckCounts(["sys-a"]);
135
+
136
+ expect(counts).toEqual({ "sys-a": 1 });
137
+ expect(Object.keys(counts)).not.toContain("sys-other");
138
+ });
139
+
140
+ it("returns an empty map for an empty request without querying", async () => {
141
+ expect(await service.getBulkAssignedHealthCheckCounts([])).toEqual({});
142
+ });
143
+ },
144
+ );
@@ -0,0 +1,197 @@
1
+ /**
2
+ * Integration test for `HealthCheckService.getBulkRunStats` against a REAL
3
+ * Postgres. The method's whole point is the single grouped read over
4
+ * `health_check_runs` for MANY systems, then per-system summarization - so each
5
+ * entry MUST be byte-identical to what the single `getRunStats({ systemId })`
6
+ * would return for the same window. A mocked db cannot prove the `inArray`
7
+ * grouping / windowing, so this real-DB guard pins the equivalence, the
8
+ * per-system isolation, and the "systems with no runs are omitted" behaviour
9
+ * the status-page uptime column relies on.
10
+ *
11
+ * Gated on CHECKSTACK_IT so it runs in CI (shared compose Postgres) and is
12
+ * skipped in the default `bun test` run, matching the other *.it.test.ts here.
13
+ */
14
+ import { afterAll, beforeAll, beforeEach, describe, expect, it } from "bun:test";
15
+ import { drizzle } from "drizzle-orm/node-postgres";
16
+ import { Pool } from "pg";
17
+ import type {
18
+ SafeDatabase,
19
+ HealthCheckRegistry,
20
+ CollectorRegistry,
21
+ } from "@checkstack/backend-api";
22
+ import * as schema from "./schema";
23
+ import { HealthCheckService } from "./service";
24
+
25
+ const PG_URL =
26
+ process.env.CHECKSTACK_IT_PG_URL ??
27
+ "postgres://postgres:postgres@localhost:5432/postgres";
28
+ const SCHEMA = "healthcheck_it_bulk_run_stats";
29
+
30
+ const START = new Date("2026-06-01T00:00:00.000Z");
31
+ const END = new Date("2026-06-01T23:59:59.000Z");
32
+
33
+ let admin: Pool;
34
+ let pool: Pool;
35
+ let service: HealthCheckService;
36
+
37
+ async function insertRun(row: {
38
+ systemId: string;
39
+ status: string;
40
+ latencyMs?: number | null;
41
+ at: string;
42
+ environmentId?: string | null;
43
+ }): Promise<void> {
44
+ await pool.query(
45
+ `INSERT INTO "${SCHEMA}".health_check_runs
46
+ (id, configuration_id, system_id, environment_id, status, latency_ms, timestamp)
47
+ VALUES ($1, $2, $3, $4, $5, $6, $7)`,
48
+ [
49
+ crypto.randomUUID(),
50
+ crypto.randomUUID(),
51
+ row.systemId,
52
+ row.environmentId ?? null,
53
+ row.status,
54
+ row.latencyMs ?? null,
55
+ row.at,
56
+ ],
57
+ );
58
+ }
59
+
60
+ describe.skipIf(!process.env.CHECKSTACK_IT)(
61
+ "HealthCheckService.getBulkRunStats (shared Postgres)",
62
+ () => {
63
+ beforeAll(async () => {
64
+ admin = new Pool({ connectionString: PG_URL });
65
+ await admin.query(`DROP SCHEMA IF EXISTS "${SCHEMA}" CASCADE`);
66
+ await admin.query(`CREATE SCHEMA "${SCHEMA}"`);
67
+ // Minimal DDL: only the columns the grouped read touches; status is plain
68
+ // text and configuration_id carries no FK, keeping the schema
69
+ // self-contained like the sibling *.it.test.ts files.
70
+ await admin.query(
71
+ `CREATE TABLE "${SCHEMA}".health_check_runs (
72
+ id uuid PRIMARY KEY,
73
+ configuration_id uuid NOT NULL,
74
+ system_id text NOT NULL,
75
+ environment_id text,
76
+ status text NOT NULL,
77
+ latency_ms integer,
78
+ result jsonb,
79
+ source_id text,
80
+ source_label text,
81
+ timestamp timestamp NOT NULL DEFAULT now()
82
+ )`,
83
+ );
84
+ pool = new Pool({
85
+ connectionString: PG_URL,
86
+ options: `-c search_path=${SCHEMA}`,
87
+ });
88
+ const db = drizzle(pool, {
89
+ schema,
90
+ }) as unknown as SafeDatabase<typeof schema>;
91
+ // registry / collectorRegistry are unused by the stats read under test;
92
+ // stub them so the constructor is satisfied.
93
+ service = new HealthCheckService(
94
+ db,
95
+ {} as unknown as HealthCheckRegistry,
96
+ {} as unknown as CollectorRegistry,
97
+ );
98
+ });
99
+
100
+ afterAll(async () => {
101
+ await pool?.end();
102
+ await admin.query(`DROP SCHEMA IF EXISTS "${SCHEMA}" CASCADE`);
103
+ await admin.end();
104
+ });
105
+
106
+ beforeEach(async () => {
107
+ await pool.query(`TRUNCATE "${SCHEMA}".health_check_runs`);
108
+ });
109
+
110
+ it("returns per-system stats identical to getRunStats for the same window", async () => {
111
+ // sys-a: 3 healthy + 1 unhealthy; sys-b: 2 healthy; sys-c: no runs.
112
+ await insertRun({ systemId: "sys-a", status: "healthy", latencyMs: 10, at: "2026-06-01T01:00:00Z" });
113
+ await insertRun({ systemId: "sys-a", status: "healthy", latencyMs: 20, at: "2026-06-01T02:00:00Z" });
114
+ await insertRun({ systemId: "sys-a", status: "healthy", latencyMs: 30, at: "2026-06-01T03:00:00Z" });
115
+ await insertRun({ systemId: "sys-a", status: "unhealthy", latencyMs: 40, at: "2026-06-01T04:00:00Z" });
116
+ await insertRun({ systemId: "sys-b", status: "healthy", latencyMs: 5, at: "2026-06-01T05:00:00Z" });
117
+ await insertRun({ systemId: "sys-b", status: "healthy", latencyMs: 7, at: "2026-06-01T06:00:00Z" });
118
+ // A run OUTSIDE the window must be ignored by both endpoints.
119
+ await insertRun({ systemId: "sys-a", status: "unhealthy", at: "2026-05-01T00:00:00Z" });
120
+
121
+ const ids = ["sys-a", "sys-b", "sys-c"];
122
+ const stats = await service.getBulkRunStats({
123
+ systemIds: ids,
124
+ startDate: START,
125
+ endDate: END,
126
+ maxBuckets: 24,
127
+ });
128
+
129
+ for (const systemId of ids) {
130
+ const single = await service.getRunStats({ systemId, startDate: START, endDate: END, maxBuckets: 24 });
131
+ if (single.total.runCount === 0) {
132
+ // Zero-run systems are omitted from the bulk record.
133
+ expect(stats[systemId]).toBeUndefined();
134
+ } else {
135
+ expect(stats[systemId]).toEqual(single);
136
+ }
137
+ }
138
+
139
+ // sys-c had no runs => omitted; only populated systems are keys.
140
+ expect(Object.keys(stats).toSorted()).toEqual(["sys-a", "sys-b"]);
141
+ // Sanity: sys-a uptime = 3/4 healthy = 75%.
142
+ expect(stats["sys-a"]?.total.uptimePct).toBe(75);
143
+ });
144
+
145
+ it("scopes uptime to the selected environments (status-page env filter)", async () => {
146
+ // sys-a: prod 2 healthy; staging 1 healthy + 1 unhealthy; env-less 1
147
+ // unhealthy. A prod-only page must count ONLY the two prod runs (100%),
148
+ // never the staging or env-less runs.
149
+ await insertRun({ systemId: "sys-a", status: "healthy", environmentId: "env-prod", at: "2026-06-01T01:00:00Z" });
150
+ await insertRun({ systemId: "sys-a", status: "healthy", environmentId: "env-prod", at: "2026-06-01T02:00:00Z" });
151
+ await insertRun({ systemId: "sys-a", status: "healthy", environmentId: "env-stage", at: "2026-06-01T03:00:00Z" });
152
+ await insertRun({ systemId: "sys-a", status: "unhealthy", environmentId: "env-stage", at: "2026-06-01T04:00:00Z" });
153
+ await insertRun({ systemId: "sys-a", status: "unhealthy", environmentId: null, at: "2026-06-01T05:00:00Z" });
154
+
155
+ const prodOnly = await service.getBulkRunStats({
156
+ systemIds: ["sys-a"],
157
+ startDate: START,
158
+ endDate: END,
159
+ environmentIds: ["env-prod"],
160
+ maxBuckets: 24,
161
+ });
162
+ expect(prodOnly["sys-a"]?.total.runCount).toBe(2);
163
+ expect(prodOnly["sys-a"]?.total.uptimePct).toBe(100);
164
+
165
+ // No env filter counts every run (5 total, 3 healthy = 60%).
166
+ const all = await service.getBulkRunStats({
167
+ systemIds: ["sys-a"],
168
+ startDate: START,
169
+ endDate: END,
170
+ maxBuckets: 24,
171
+ });
172
+ expect(all["sys-a"]?.total.runCount).toBe(5);
173
+ expect(all["sys-a"]?.total.uptimePct).toBe(60);
174
+
175
+ // getRunStats honors the same set filter for the single-system uptime widget.
176
+ const single = await service.getRunStats({
177
+ systemId: "sys-a",
178
+ startDate: START,
179
+ endDate: END,
180
+ environmentIds: ["env-prod", "env-stage"],
181
+ maxBuckets: 24,
182
+ });
183
+ expect(single.total.runCount).toBe(4);
184
+ });
185
+
186
+ it("returns an empty record for an empty request without querying", async () => {
187
+ expect(
188
+ await service.getBulkRunStats({
189
+ systemIds: [],
190
+ startDate: START,
191
+ endDate: END,
192
+ maxBuckets: 1,
193
+ }),
194
+ ).toEqual({});
195
+ });
196
+ },
197
+ );
@@ -1,5 +1,6 @@
1
1
  import { describe, it, expect, mock, beforeEach } from "bun:test";
2
2
  import type { InferSelectModel } from "drizzle-orm";
3
+ import { withTransactionMock } from "@checkstack/test-utils-backend";
3
4
  import { HealthCheckService } from "./service";
4
5
  import {
5
6
  healthCheckRuns,
@@ -64,6 +65,10 @@ describe("HealthCheckService data ordering", () => {
64
65
  const whereMock = mock(() => ({
65
66
  orderBy: orderByMock,
66
67
  limit: mock(createLimitResult),
68
+ // getSystemHealthOverview's "last successful run" query terminates in
69
+ // `.where(...).groupBy(environmentId)` (a max-per-env aggregate). These
70
+ // ordering tests don't assert last-success, so return no groups.
71
+ groupBy: mock(() => Promise.resolve([])),
67
72
  }));
68
73
  const innerJoinMock = mock(() => ({
69
74
  where: mock(() => Promise.resolve([...mockAssociations])),
@@ -74,10 +79,13 @@ describe("HealthCheckService data ordering", () => {
74
79
  orderBy: orderByMock,
75
80
  }));
76
81
 
77
- return {
82
+ // getSystemHealthOverview batches its reads in ONE scoped transaction, so
83
+ // the mock must expose `.transaction(cb)` (runs `cb` against the same mock
84
+ // db). getHistory/getDetailedHistory run standalone and ignore it.
85
+ return withTransactionMock({
78
86
  select: mock(() => ({ from: fromMock })),
79
87
  $count: mock(() => Promise.resolve(mockRuns.length)),
80
- };
88
+ });
81
89
  }
82
90
 
83
91
  beforeEach(() => {
@@ -1,4 +1,5 @@
1
1
  import { describe, it, expect, mock, beforeEach } from "bun:test";
2
+ import { withTransactionMock } from "@checkstack/test-utils-backend";
2
3
  import { HealthCheckService } from "./service";
3
4
 
4
5
  /**
@@ -92,6 +93,12 @@ describe("HealthCheckService - paused configuration filtering", () => {
92
93
  })),
93
94
  });
94
95
 
96
+ // Distinct env keys query (rollup): resolves to no env slices for these
97
+ // fixtures (runs are empty), so the rollup has nothing to evaluate.
98
+ const distinctFrom = Object.assign(Promise.resolve([]), {
99
+ where: mock(() => Promise.resolve([])),
100
+ });
101
+
95
102
  let selectCallCount = 0;
96
103
  return {
97
104
  select: mock(() => {
@@ -103,6 +110,7 @@ describe("HealthCheckService - paused configuration filtering", () => {
103
110
  }
104
111
  return { from: mock(() => runsFrom) };
105
112
  }),
113
+ selectDistinct: mock(() => ({ from: mock(() => distinctFrom) })),
106
114
  insert: mock(() => ({
107
115
  values: mock(() => ({
108
116
  onConflictDoUpdate: mock(() => Promise.resolve()),
@@ -143,7 +151,7 @@ describe("HealthCheckService - paused configuration filtering", () => {
143
151
 
144
152
  const mockDb = createMockDb();
145
153
  const service = new HealthCheckService(
146
- mockDb as never,
154
+ withTransactionMock(mockDb) as never,
147
155
  {} as never,
148
156
  {} as never,
149
157
  );
@@ -198,6 +206,11 @@ describe("HealthCheckService - paused configuration filtering", () => {
198
206
  orderBy: runsOrderBy,
199
207
  });
200
208
 
209
+ // Distinct env keys: a single env-less (null) slice for this env-less check.
210
+ const distinctFrom = Object.assign(Promise.resolve([]), {
211
+ where: mock(() => Promise.resolve([{ environmentId: null }])),
212
+ });
213
+
201
214
  let selectCallCount = 0;
202
215
  const mockDb = {
203
216
  select: mock(() => {
@@ -207,6 +220,7 @@ describe("HealthCheckService - paused configuration filtering", () => {
207
220
  }
208
221
  return { from: mock(() => runsFrom) };
209
222
  }),
223
+ selectDistinct: mock(() => ({ from: mock(() => distinctFrom) })),
210
224
  insert: mock(() => ({
211
225
  values: mock(() => ({
212
226
  onConflictDoUpdate: mock(() => Promise.resolve()),
@@ -222,7 +236,7 @@ describe("HealthCheckService - paused configuration filtering", () => {
222
236
  };
223
237
 
224
238
  const service = new HealthCheckService(
225
- mockDb as never,
239
+ withTransactionMock(mockDb) as never,
226
240
  {} as never,
227
241
  {} as never,
228
242
  );
@@ -239,7 +253,7 @@ describe("HealthCheckService - paused configuration filtering", () => {
239
253
 
240
254
  const mockDb = createMockDb();
241
255
  const service = new HealthCheckService(
242
- mockDb as never,
256
+ withTransactionMock(mockDb) as never,
243
257
  {} as never,
244
258
  {} as never,
245
259
  );
@@ -265,7 +279,7 @@ describe("HealthCheckService - paused configuration filtering", () => {
265
279
  };
266
280
 
267
281
  const service = new HealthCheckService(
268
- mockDb as never,
282
+ withTransactionMock(mockDb) as never,
269
283
  {} as never,
270
284
  {} as never,
271
285
  );
@@ -285,7 +299,7 @@ describe("HealthCheckService - paused configuration filtering", () => {
285
299
  };
286
300
 
287
301
  const service = new HealthCheckService(
288
- mockDb as never,
302
+ withTransactionMock(mockDb) as never,
289
303
  {} as never,
290
304
  {} as never,
291
305
  );
@@ -340,7 +354,13 @@ describe("HealthCheckService - paused configuration filtering", () => {
340
354
 
341
355
  const runsLimit = mock(() => Promise.resolve(emptyRuns));
342
356
  const runsOrderBy = mock(() => ({ limit: runsLimit }));
343
- const runsWhere = mock(() => ({ orderBy: runsOrderBy, limit: runsLimit }));
357
+ // getSystemHealthOverview also runs a `.where(...).groupBy(env)` aggregate
358
+ // for the last-successful-run stamp; return no groups here.
359
+ const runsWhere = mock(() => ({
360
+ orderBy: runsOrderBy,
361
+ limit: runsLimit,
362
+ groupBy: mock(() => Promise.resolve([])),
363
+ }));
344
364
  const runsFrom = Object.assign(Promise.resolve(emptyRuns), {
345
365
  where: runsWhere,
346
366
  orderBy: runsOrderBy,
@@ -370,7 +390,7 @@ describe("HealthCheckService - paused configuration filtering", () => {
370
390
  };
371
391
 
372
392
  const service = new HealthCheckService(
373
- mockDb as never,
393
+ withTransactionMock(mockDb) as never,
374
394
  {} as never,
375
395
  {} as never,
376
396
  );