@checkstack/healthcheck-backend 0.0.2
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 +181 -0
- package/drizzle/0000_stormy_slayback.sql +33 -0
- package/drizzle/0001_thin_shotgun.sql +1 -0
- package/drizzle/0002_closed_lucky_pierre.sql +19 -0
- package/drizzle/0003_powerful_rage.sql +1 -0
- package/drizzle/0004_short_ezekiel.sql +1 -0
- package/drizzle/0005_glossy_longshot.sql +1 -0
- package/drizzle/meta/0000_snapshot.json +234 -0
- package/drizzle/meta/0001_snapshot.json +240 -0
- package/drizzle/meta/0002_snapshot.json +361 -0
- package/drizzle/meta/0003_snapshot.json +367 -0
- package/drizzle/meta/0004_snapshot.json +401 -0
- package/drizzle/meta/0005_snapshot.json +401 -0
- package/drizzle/meta/_journal.json +48 -0
- package/drizzle.config.ts +7 -0
- package/package.json +37 -0
- package/src/aggregation.test.ts +373 -0
- package/src/hooks.test.ts +16 -0
- package/src/hooks.ts +35 -0
- package/src/index.ts +195 -0
- package/src/queue-executor.test.ts +229 -0
- package/src/queue-executor.ts +569 -0
- package/src/retention-job.ts +404 -0
- package/src/router.test.ts +81 -0
- package/src/router.ts +157 -0
- package/src/schema.ts +153 -0
- package/src/service.ts +718 -0
- package/src/state-evaluator.test.ts +237 -0
- package/src/state-evaluator.ts +105 -0
- package/src/state-thresholds-migrations.ts +15 -0
- package/tsconfig.json +6 -0
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
setupHealthCheckWorker,
|
|
4
|
+
scheduleHealthCheck,
|
|
5
|
+
bootstrapHealthChecks,
|
|
6
|
+
type HealthCheckJobPayload,
|
|
7
|
+
} from "./queue-executor";
|
|
8
|
+
import {
|
|
9
|
+
createMockLogger,
|
|
10
|
+
createMockQueueManager,
|
|
11
|
+
createMockDb,
|
|
12
|
+
createMockSignalService,
|
|
13
|
+
} from "@checkstack/test-utils-backend";
|
|
14
|
+
import {
|
|
15
|
+
type HealthCheckRegistry,
|
|
16
|
+
Versioned,
|
|
17
|
+
z,
|
|
18
|
+
} from "@checkstack/backend-api";
|
|
19
|
+
import { mock } from "bun:test";
|
|
20
|
+
|
|
21
|
+
// Helper to create mock health check registry
|
|
22
|
+
const createMockRegistry = (): HealthCheckRegistry => ({
|
|
23
|
+
getStrategy: mock((id: string) => ({
|
|
24
|
+
id,
|
|
25
|
+
displayName: "Mock Strategy",
|
|
26
|
+
description: "Mock",
|
|
27
|
+
config: new Versioned({
|
|
28
|
+
version: 1,
|
|
29
|
+
schema: z.object({}),
|
|
30
|
+
}),
|
|
31
|
+
aggregatedResult: new Versioned({
|
|
32
|
+
version: 1,
|
|
33
|
+
schema: z.object({}),
|
|
34
|
+
}),
|
|
35
|
+
execute: mock(async () => ({
|
|
36
|
+
status: "healthy" as const,
|
|
37
|
+
message: "Mock check passed",
|
|
38
|
+
timestamp: new Date().toISOString(),
|
|
39
|
+
})),
|
|
40
|
+
aggregateResult: mock(() => ({})),
|
|
41
|
+
})),
|
|
42
|
+
register: mock(() => {}),
|
|
43
|
+
getStrategies: mock(() => []),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Helper to create mock catalog client for notification delegation
|
|
47
|
+
const createMockCatalogClient = () => ({
|
|
48
|
+
notifySystemSubscribers: mock(async () => ({ notifiedCount: 0 })),
|
|
49
|
+
// Other methods not used in queue-executor
|
|
50
|
+
getEntities: mock(async () => ({ systems: [], groups: [] })),
|
|
51
|
+
getSystems: mock(async () => []),
|
|
52
|
+
getGroups: mock(async () => []),
|
|
53
|
+
createSystem: mock(async () => ({})),
|
|
54
|
+
updateSystem: mock(async () => ({})),
|
|
55
|
+
deleteSystem: mock(async () => ({ success: true })),
|
|
56
|
+
createGroup: mock(async () => ({})),
|
|
57
|
+
updateGroup: mock(async () => ({})),
|
|
58
|
+
deleteGroup: mock(async () => ({ success: true })),
|
|
59
|
+
addSystemToGroup: mock(async () => ({ success: true })),
|
|
60
|
+
removeSystemFromGroup: mock(async () => ({ success: true })),
|
|
61
|
+
getViews: mock(async () => []),
|
|
62
|
+
createView: mock(async () => ({})),
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe("Queue-Based Health Check Executor", () => {
|
|
66
|
+
describe("scheduleHealthCheck", () => {
|
|
67
|
+
it("should enqueue a health check with delay and deterministic jobId", async () => {
|
|
68
|
+
const mockQueueManager = createMockQueueManager();
|
|
69
|
+
const mockLogger = createMockLogger();
|
|
70
|
+
|
|
71
|
+
const payload: HealthCheckJobPayload = {
|
|
72
|
+
configId: "config-1",
|
|
73
|
+
systemId: "system-1",
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
await scheduleHealthCheck({
|
|
77
|
+
queueManager: mockQueueManager,
|
|
78
|
+
payload,
|
|
79
|
+
intervalSeconds: 60,
|
|
80
|
+
logger: mockLogger,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Verify queue was created with correct name
|
|
84
|
+
const queue = mockQueueManager.getQueue("health-checks");
|
|
85
|
+
expect(queue).toBeDefined();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("should use deterministic job IDs", async () => {
|
|
89
|
+
const mockQueueManager = createMockQueueManager();
|
|
90
|
+
const mockLogger = createMockLogger();
|
|
91
|
+
|
|
92
|
+
const payload: HealthCheckJobPayload = {
|
|
93
|
+
configId: "config-1",
|
|
94
|
+
systemId: "system-1",
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const result = await scheduleHealthCheck({
|
|
98
|
+
queueManager: mockQueueManager,
|
|
99
|
+
payload,
|
|
100
|
+
intervalSeconds: 60,
|
|
101
|
+
logger: mockLogger,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// The result should be a job ID
|
|
105
|
+
expect(result).toBeDefined();
|
|
106
|
+
expect(typeof result).toBe("string");
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe("setupHealthCheckWorker", () => {
|
|
111
|
+
it("should subscribe to the health-checks queue in work-queue mode", async () => {
|
|
112
|
+
const mockDb = createMockDb();
|
|
113
|
+
const mockRegistry = createMockRegistry();
|
|
114
|
+
const mockLogger = createMockLogger();
|
|
115
|
+
const mockQueueManager = createMockQueueManager();
|
|
116
|
+
const mockCatalogClient = createMockCatalogClient();
|
|
117
|
+
|
|
118
|
+
await setupHealthCheckWorker({
|
|
119
|
+
db: mockDb as unknown as Parameters<
|
|
120
|
+
typeof setupHealthCheckWorker
|
|
121
|
+
>[0]["db"],
|
|
122
|
+
registry: mockRegistry,
|
|
123
|
+
logger: mockLogger,
|
|
124
|
+
queueManager: mockQueueManager,
|
|
125
|
+
signalService: createMockSignalService(),
|
|
126
|
+
catalogClient: mockCatalogClient as unknown as Parameters<
|
|
127
|
+
typeof setupHealthCheckWorker
|
|
128
|
+
>[0]["catalogClient"],
|
|
129
|
+
getEmitHook: () => undefined,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
expect(mockLogger.debug).toHaveBeenCalledWith(
|
|
133
|
+
expect.stringContaining("Health Check Worker subscribed")
|
|
134
|
+
);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe("bootstrapHealthChecks", () => {
|
|
139
|
+
beforeEach(() => {
|
|
140
|
+
// Reset all mocks between tests
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("should enqueue all enabled health checks", async () => {
|
|
144
|
+
const mockQueueManager = createMockQueueManager();
|
|
145
|
+
const mockLogger = createMockLogger();
|
|
146
|
+
const mockDb = createMockDb();
|
|
147
|
+
|
|
148
|
+
// Configure the mock database to return some enabled checks
|
|
149
|
+
const mockData = [
|
|
150
|
+
{
|
|
151
|
+
systemId: "system-1",
|
|
152
|
+
configId: "config-1",
|
|
153
|
+
interval: 30,
|
|
154
|
+
lastRun: null,
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
systemId: "system-2",
|
|
158
|
+
configId: "config-2",
|
|
159
|
+
interval: 60,
|
|
160
|
+
lastRun: null,
|
|
161
|
+
},
|
|
162
|
+
];
|
|
163
|
+
|
|
164
|
+
// Override select to return a chain that handles subquery with groupBy
|
|
165
|
+
// First call: for enabledChecks query (innerJoin().where)
|
|
166
|
+
// Second call: for latestRuns query (groupBy)
|
|
167
|
+
let selectCallCount = 0;
|
|
168
|
+
(mockDb.select as any) = mock(() => {
|
|
169
|
+
selectCallCount++;
|
|
170
|
+
if (selectCallCount === 1) {
|
|
171
|
+
// enabledChecks query
|
|
172
|
+
return {
|
|
173
|
+
from: mock(() => ({
|
|
174
|
+
innerJoin: mock(() => ({
|
|
175
|
+
where: mock(() => Promise.resolve(mockData)),
|
|
176
|
+
})),
|
|
177
|
+
})),
|
|
178
|
+
};
|
|
179
|
+
} else {
|
|
180
|
+
// latestRuns query
|
|
181
|
+
return {
|
|
182
|
+
from: mock(() => ({
|
|
183
|
+
groupBy: mock(() => Promise.resolve([])),
|
|
184
|
+
})),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
await bootstrapHealthChecks({
|
|
190
|
+
db: mockDb as any,
|
|
191
|
+
queueManager: mockQueueManager,
|
|
192
|
+
logger: mockLogger,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
expect(mockLogger.debug).toHaveBeenCalledWith(
|
|
196
|
+
"Bootstrapping 2 health checks"
|
|
197
|
+
);
|
|
198
|
+
expect(mockLogger.debug).toHaveBeenCalledWith(
|
|
199
|
+
"✅ Bootstrapped 2 health checks"
|
|
200
|
+
);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("should handle empty health check list", async () => {
|
|
204
|
+
const mockQueueManager = createMockQueueManager();
|
|
205
|
+
const mockLogger = createMockLogger();
|
|
206
|
+
const mockDb = createMockDb();
|
|
207
|
+
|
|
208
|
+
// Override to return empty array
|
|
209
|
+
const mockSelectChain = mockDb.select();
|
|
210
|
+
const mockFromResult = (mockSelectChain as any).from();
|
|
211
|
+
Object.assign(mockFromResult, {
|
|
212
|
+
then: (resolve: any) => resolve([]),
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
await bootstrapHealthChecks({
|
|
216
|
+
db: mockDb as any,
|
|
217
|
+
queueManager: mockQueueManager,
|
|
218
|
+
logger: mockLogger,
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
expect(mockLogger.debug).toHaveBeenCalledWith(
|
|
222
|
+
"Bootstrapping 0 health checks"
|
|
223
|
+
);
|
|
224
|
+
expect(mockLogger.debug).toHaveBeenCalledWith(
|
|
225
|
+
"✅ Bootstrapped 0 health checks"
|
|
226
|
+
);
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
});
|