@checkstack/dependency-backend 0.2.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 +44 -0
- package/drizzle/0000_safe_dark_phoenix.sql +30 -0
- package/drizzle/0001_cooing_bedlam.sql +5 -0
- package/drizzle/meta/0000_snapshot.json +206 -0
- package/drizzle/meta/0001_snapshot.json +238 -0
- package/drizzle/meta/_journal.json +20 -0
- package/drizzle.config.ts +7 -0
- package/package.json +38 -0
- package/src/hooks.ts +36 -0
- package/src/index.ts +208 -0
- package/src/notifications.ts +339 -0
- package/src/router.ts +305 -0
- package/src/schema.ts +82 -0
- package/src/services/dependency-service.ts +376 -0
- package/src/services/warning-evaluation-service.ts +305 -0
- package/tests/notifications.test.ts +155 -0
- package/tests/warning-evaluation-service.test.ts +773 -0
- package/tsconfig.json +6 -0
|
@@ -0,0 +1,773 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { WarningEvaluationService } from "../src/services/warning-evaluation-service";
|
|
3
|
+
import type { SystemStatus } from "../src/services/warning-evaluation-service";
|
|
4
|
+
import type { Dependency } from "@checkstack/dependency-common";
|
|
5
|
+
|
|
6
|
+
function makeDependency(
|
|
7
|
+
overrides: Partial<Dependency> & {
|
|
8
|
+
sourceSystemId: string;
|
|
9
|
+
targetSystemId: string;
|
|
10
|
+
},
|
|
11
|
+
): Dependency {
|
|
12
|
+
return {
|
|
13
|
+
id: crypto.randomUUID(),
|
|
14
|
+
impactType: "degraded",
|
|
15
|
+
transitive: false,
|
|
16
|
+
// eslint-disable-next-line unicorn/no-null -- Drizzle schema uses null
|
|
17
|
+
label: null,
|
|
18
|
+
createdAt: new Date(),
|
|
19
|
+
updatedAt: new Date(),
|
|
20
|
+
...overrides,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function makeSystemStatus(
|
|
25
|
+
overrides: Partial<SystemStatus> & { systemId: string },
|
|
26
|
+
): SystemStatus {
|
|
27
|
+
return {
|
|
28
|
+
systemName: `System ${overrides.systemId}`,
|
|
29
|
+
status: "operational",
|
|
30
|
+
...overrides,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe("WarningEvaluationService", () => {
|
|
35
|
+
const service = new WarningEvaluationService();
|
|
36
|
+
|
|
37
|
+
describe("evaluateWarnings", () => {
|
|
38
|
+
test("returns empty map when no dependencies exist", () => {
|
|
39
|
+
const result = service.evaluateWarnings({
|
|
40
|
+
systemIds: ["sys-a"],
|
|
41
|
+
allDependencies: [],
|
|
42
|
+
systemStatuses: new Map([
|
|
43
|
+
["sys-a", makeSystemStatus({ systemId: "sys-a" })],
|
|
44
|
+
]),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
expect(result.size).toBe(0);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("returns no warning when upstream is operational", () => {
|
|
51
|
+
const result = service.evaluateWarnings({
|
|
52
|
+
systemIds: ["sys-a"],
|
|
53
|
+
allDependencies: [
|
|
54
|
+
makeDependency({
|
|
55
|
+
sourceSystemId: "sys-a",
|
|
56
|
+
targetSystemId: "sys-b",
|
|
57
|
+
impactType: "critical",
|
|
58
|
+
}),
|
|
59
|
+
],
|
|
60
|
+
systemStatuses: new Map([
|
|
61
|
+
["sys-a", makeSystemStatus({ systemId: "sys-a" })],
|
|
62
|
+
[
|
|
63
|
+
"sys-b",
|
|
64
|
+
makeSystemStatus({ systemId: "sys-b", status: "operational" }),
|
|
65
|
+
],
|
|
66
|
+
]),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
expect(result.size).toBe(0);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("returns degraded warning for degraded upstream with degraded impact", () => {
|
|
73
|
+
const result = service.evaluateWarnings({
|
|
74
|
+
systemIds: ["sys-a"],
|
|
75
|
+
allDependencies: [
|
|
76
|
+
makeDependency({
|
|
77
|
+
sourceSystemId: "sys-a",
|
|
78
|
+
targetSystemId: "sys-b",
|
|
79
|
+
impactType: "degraded",
|
|
80
|
+
}),
|
|
81
|
+
],
|
|
82
|
+
systemStatuses: new Map([
|
|
83
|
+
["sys-a", makeSystemStatus({ systemId: "sys-a" })],
|
|
84
|
+
[
|
|
85
|
+
"sys-b",
|
|
86
|
+
makeSystemStatus({ systemId: "sys-b", status: "degraded" }),
|
|
87
|
+
],
|
|
88
|
+
]),
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
expect(result.size).toBe(1);
|
|
92
|
+
const warning = result.get("sys-a")!;
|
|
93
|
+
expect(warning.derivedState).toBe("degraded");
|
|
94
|
+
expect(warning.affectedUpstreams).toHaveLength(1);
|
|
95
|
+
expect(warning.affectedUpstreams[0].systemId).toBe("sys-b");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("returns degraded warning for degraded upstream with critical impact", () => {
|
|
99
|
+
const result = service.evaluateWarnings({
|
|
100
|
+
systemIds: ["sys-a"],
|
|
101
|
+
allDependencies: [
|
|
102
|
+
makeDependency({
|
|
103
|
+
sourceSystemId: "sys-a",
|
|
104
|
+
targetSystemId: "sys-b",
|
|
105
|
+
impactType: "critical",
|
|
106
|
+
}),
|
|
107
|
+
],
|
|
108
|
+
systemStatuses: new Map([
|
|
109
|
+
["sys-a", makeSystemStatus({ systemId: "sys-a" })],
|
|
110
|
+
[
|
|
111
|
+
"sys-b",
|
|
112
|
+
makeSystemStatus({ systemId: "sys-b", status: "degraded" }),
|
|
113
|
+
],
|
|
114
|
+
]),
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const warning = result.get("sys-a")!;
|
|
118
|
+
expect(warning.derivedState).toBe("degraded");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("returns down warning for down upstream with critical impact", () => {
|
|
122
|
+
const result = service.evaluateWarnings({
|
|
123
|
+
systemIds: ["sys-a"],
|
|
124
|
+
allDependencies: [
|
|
125
|
+
makeDependency({
|
|
126
|
+
sourceSystemId: "sys-a",
|
|
127
|
+
targetSystemId: "sys-b",
|
|
128
|
+
impactType: "critical",
|
|
129
|
+
}),
|
|
130
|
+
],
|
|
131
|
+
systemStatuses: new Map([
|
|
132
|
+
["sys-a", makeSystemStatus({ systemId: "sys-a" })],
|
|
133
|
+
["sys-b", makeSystemStatus({ systemId: "sys-b", status: "down" })],
|
|
134
|
+
]),
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const warning = result.get("sys-a")!;
|
|
138
|
+
expect(warning.derivedState).toBe("down");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("returns info warning for informational impact type", () => {
|
|
142
|
+
const result = service.evaluateWarnings({
|
|
143
|
+
systemIds: ["sys-a"],
|
|
144
|
+
allDependencies: [
|
|
145
|
+
makeDependency({
|
|
146
|
+
sourceSystemId: "sys-a",
|
|
147
|
+
targetSystemId: "sys-b",
|
|
148
|
+
impactType: "informational",
|
|
149
|
+
}),
|
|
150
|
+
],
|
|
151
|
+
systemStatuses: new Map([
|
|
152
|
+
["sys-a", makeSystemStatus({ systemId: "sys-a" })],
|
|
153
|
+
["sys-b", makeSystemStatus({ systemId: "sys-b", status: "down" })],
|
|
154
|
+
]),
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const warning = result.get("sys-a")!;
|
|
158
|
+
expect(warning.derivedState).toBe("info");
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
describe("worst state aggregation", () => {
|
|
163
|
+
test("selects the worst state across multiple upstream failures", () => {
|
|
164
|
+
const result = service.evaluateWarnings({
|
|
165
|
+
systemIds: ["sys-a"],
|
|
166
|
+
allDependencies: [
|
|
167
|
+
makeDependency({
|
|
168
|
+
sourceSystemId: "sys-a",
|
|
169
|
+
targetSystemId: "sys-b",
|
|
170
|
+
impactType: "informational",
|
|
171
|
+
}),
|
|
172
|
+
makeDependency({
|
|
173
|
+
sourceSystemId: "sys-a",
|
|
174
|
+
targetSystemId: "sys-c",
|
|
175
|
+
impactType: "critical",
|
|
176
|
+
}),
|
|
177
|
+
],
|
|
178
|
+
systemStatuses: new Map([
|
|
179
|
+
["sys-a", makeSystemStatus({ systemId: "sys-a" })],
|
|
180
|
+
["sys-b", makeSystemStatus({ systemId: "sys-b", status: "down" })],
|
|
181
|
+
[
|
|
182
|
+
"sys-c",
|
|
183
|
+
makeSystemStatus({ systemId: "sys-c", status: "degraded" }),
|
|
184
|
+
],
|
|
185
|
+
]),
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const warning = result.get("sys-a")!;
|
|
189
|
+
// info from sys-b (informational + down = info)
|
|
190
|
+
// degraded from sys-c (critical + degraded = degraded)
|
|
191
|
+
// worst: degraded
|
|
192
|
+
expect(warning.derivedState).toBe("degraded");
|
|
193
|
+
expect(warning.affectedUpstreams).toHaveLength(2);
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
describe("transitive propagation", () => {
|
|
198
|
+
test("propagates warnings through transitive dependency chains", () => {
|
|
199
|
+
// sys-a depends on sys-b (transitive), sys-b depends on sys-c
|
|
200
|
+
// sys-c is down, but sys-b is still operational
|
|
201
|
+
// With transitive: sys-a should see degradation through sys-b
|
|
202
|
+
const result = service.evaluateWarnings({
|
|
203
|
+
systemIds: ["sys-a"],
|
|
204
|
+
allDependencies: [
|
|
205
|
+
makeDependency({
|
|
206
|
+
sourceSystemId: "sys-a",
|
|
207
|
+
targetSystemId: "sys-b",
|
|
208
|
+
impactType: "critical",
|
|
209
|
+
transitive: true,
|
|
210
|
+
}),
|
|
211
|
+
makeDependency({
|
|
212
|
+
sourceSystemId: "sys-b",
|
|
213
|
+
targetSystemId: "sys-c",
|
|
214
|
+
impactType: "critical",
|
|
215
|
+
}),
|
|
216
|
+
],
|
|
217
|
+
systemStatuses: new Map([
|
|
218
|
+
["sys-a", makeSystemStatus({ systemId: "sys-a" })],
|
|
219
|
+
[
|
|
220
|
+
"sys-b",
|
|
221
|
+
makeSystemStatus({
|
|
222
|
+
systemId: "sys-b",
|
|
223
|
+
status: "operational",
|
|
224
|
+
}),
|
|
225
|
+
],
|
|
226
|
+
["sys-c", makeSystemStatus({ systemId: "sys-c", status: "down" })],
|
|
227
|
+
]),
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
const warning = result.get("sys-a")!;
|
|
231
|
+
expect(warning).toBeDefined();
|
|
232
|
+
// sys-b is promoted to "down" from sys-c's critical+down
|
|
233
|
+
// then sys-a gets critical+down = "down"
|
|
234
|
+
expect(warning.derivedState).toBe("down");
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test("does not propagate through non-transitive dependencies", () => {
|
|
238
|
+
// Same chain but without transitive flag
|
|
239
|
+
const result = service.evaluateWarnings({
|
|
240
|
+
systemIds: ["sys-a"],
|
|
241
|
+
allDependencies: [
|
|
242
|
+
makeDependency({
|
|
243
|
+
sourceSystemId: "sys-a",
|
|
244
|
+
targetSystemId: "sys-b",
|
|
245
|
+
impactType: "critical",
|
|
246
|
+
transitive: false,
|
|
247
|
+
}),
|
|
248
|
+
makeDependency({
|
|
249
|
+
sourceSystemId: "sys-b",
|
|
250
|
+
targetSystemId: "sys-c",
|
|
251
|
+
impactType: "critical",
|
|
252
|
+
}),
|
|
253
|
+
],
|
|
254
|
+
systemStatuses: new Map([
|
|
255
|
+
["sys-a", makeSystemStatus({ systemId: "sys-a" })],
|
|
256
|
+
[
|
|
257
|
+
"sys-b",
|
|
258
|
+
makeSystemStatus({
|
|
259
|
+
systemId: "sys-b",
|
|
260
|
+
status: "operational",
|
|
261
|
+
}),
|
|
262
|
+
],
|
|
263
|
+
["sys-c", makeSystemStatus({ systemId: "sys-c", status: "down" })],
|
|
264
|
+
]),
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// sys-b is operational (not affected), sys-a has no warning
|
|
268
|
+
expect(result.size).toBe(0);
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
describe("cycle safety", () => {
|
|
273
|
+
test("handles dependency cycles without infinite loops", () => {
|
|
274
|
+
// sys-a → sys-b → sys-c → sys-a (cycle)
|
|
275
|
+
const result = service.evaluateWarnings({
|
|
276
|
+
systemIds: ["sys-a"],
|
|
277
|
+
allDependencies: [
|
|
278
|
+
makeDependency({
|
|
279
|
+
sourceSystemId: "sys-a",
|
|
280
|
+
targetSystemId: "sys-b",
|
|
281
|
+
impactType: "critical",
|
|
282
|
+
transitive: true,
|
|
283
|
+
}),
|
|
284
|
+
makeDependency({
|
|
285
|
+
sourceSystemId: "sys-b",
|
|
286
|
+
targetSystemId: "sys-c",
|
|
287
|
+
impactType: "critical",
|
|
288
|
+
transitive: true,
|
|
289
|
+
}),
|
|
290
|
+
makeDependency({
|
|
291
|
+
sourceSystemId: "sys-c",
|
|
292
|
+
targetSystemId: "sys-a",
|
|
293
|
+
impactType: "critical",
|
|
294
|
+
transitive: true,
|
|
295
|
+
}),
|
|
296
|
+
],
|
|
297
|
+
systemStatuses: new Map([
|
|
298
|
+
[
|
|
299
|
+
"sys-a",
|
|
300
|
+
makeSystemStatus({ systemId: "sys-a", status: "operational" }),
|
|
301
|
+
],
|
|
302
|
+
[
|
|
303
|
+
"sys-b",
|
|
304
|
+
makeSystemStatus({ systemId: "sys-b", status: "degraded" }),
|
|
305
|
+
],
|
|
306
|
+
[
|
|
307
|
+
"sys-c",
|
|
308
|
+
makeSystemStatus({ systemId: "sys-c", status: "operational" }),
|
|
309
|
+
],
|
|
310
|
+
]),
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// Should not hang or throw — visited guard protects against cycles
|
|
314
|
+
expect(result).toBeDefined();
|
|
315
|
+
// sys-a depends on sys-b (degraded) → sys-a gets degraded
|
|
316
|
+
const warning = result.get("sys-a")!;
|
|
317
|
+
expect(warning).toBeDefined();
|
|
318
|
+
expect(warning.derivedState).toBe("degraded");
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
describe("health check rules", () => {
|
|
323
|
+
test("uses health check rules when available", () => {
|
|
324
|
+
const result = service.evaluateWarnings({
|
|
325
|
+
systemIds: ["sys-a"],
|
|
326
|
+
allDependencies: [
|
|
327
|
+
makeDependency({
|
|
328
|
+
sourceSystemId: "sys-a",
|
|
329
|
+
targetSystemId: "sys-b",
|
|
330
|
+
impactType: "degraded", // base impact
|
|
331
|
+
healthCheckRules: [
|
|
332
|
+
{
|
|
333
|
+
id: "rule-1",
|
|
334
|
+
dependencyId: "dep-1",
|
|
335
|
+
healthCheckId: "hc-1",
|
|
336
|
+
overrideImpactType: "critical", // overridden to critical for this check
|
|
337
|
+
},
|
|
338
|
+
],
|
|
339
|
+
}),
|
|
340
|
+
],
|
|
341
|
+
systemStatuses: new Map([
|
|
342
|
+
["sys-a", makeSystemStatus({ systemId: "sys-a" })],
|
|
343
|
+
[
|
|
344
|
+
"sys-b",
|
|
345
|
+
makeSystemStatus({
|
|
346
|
+
systemId: "sys-b",
|
|
347
|
+
status: "down",
|
|
348
|
+
healthCheckStatuses: [
|
|
349
|
+
{ healthCheckId: "hc-1", status: "unhealthy" },
|
|
350
|
+
],
|
|
351
|
+
}),
|
|
352
|
+
],
|
|
353
|
+
]),
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
const warning = result.get("sys-a")!;
|
|
357
|
+
// hc-1 is unhealthy → maps to "down" upstream equivalent
|
|
358
|
+
// With critical override: down → "down" derived state
|
|
359
|
+
expect(warning.derivedState).toBe("down");
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
test("skips healthy health checks in rules", () => {
|
|
363
|
+
const result = service.evaluateWarnings({
|
|
364
|
+
systemIds: ["sys-a"],
|
|
365
|
+
allDependencies: [
|
|
366
|
+
makeDependency({
|
|
367
|
+
sourceSystemId: "sys-a",
|
|
368
|
+
targetSystemId: "sys-b",
|
|
369
|
+
impactType: "critical",
|
|
370
|
+
healthCheckRules: [
|
|
371
|
+
{
|
|
372
|
+
id: "rule-1",
|
|
373
|
+
dependencyId: "dep-1",
|
|
374
|
+
healthCheckId: "hc-1",
|
|
375
|
+
overrideImpactType: "critical",
|
|
376
|
+
},
|
|
377
|
+
],
|
|
378
|
+
}),
|
|
379
|
+
],
|
|
380
|
+
systemStatuses: new Map([
|
|
381
|
+
["sys-a", makeSystemStatus({ systemId: "sys-a" })],
|
|
382
|
+
[
|
|
383
|
+
"sys-b",
|
|
384
|
+
makeSystemStatus({
|
|
385
|
+
systemId: "sys-b",
|
|
386
|
+
status: "down", // overall status is down
|
|
387
|
+
healthCheckStatuses: [
|
|
388
|
+
{ healthCheckId: "hc-1", status: "healthy" }, // but the specific check is healthy
|
|
389
|
+
],
|
|
390
|
+
}),
|
|
391
|
+
],
|
|
392
|
+
]),
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
// No warning because the specific health check rule targets hc-1 which is healthy
|
|
396
|
+
expect(result.size).toBe(0);
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
describe("bulk evaluation", () => {
|
|
401
|
+
test("evaluates multiple systems in a single call", () => {
|
|
402
|
+
const result = service.evaluateWarnings({
|
|
403
|
+
systemIds: ["sys-a", "sys-b", "sys-c"],
|
|
404
|
+
allDependencies: [
|
|
405
|
+
makeDependency({
|
|
406
|
+
sourceSystemId: "sys-a",
|
|
407
|
+
targetSystemId: "sys-d",
|
|
408
|
+
impactType: "critical",
|
|
409
|
+
}),
|
|
410
|
+
makeDependency({
|
|
411
|
+
sourceSystemId: "sys-b",
|
|
412
|
+
targetSystemId: "sys-d",
|
|
413
|
+
impactType: "degraded",
|
|
414
|
+
}),
|
|
415
|
+
// sys-c has no dependencies
|
|
416
|
+
],
|
|
417
|
+
systemStatuses: new Map([
|
|
418
|
+
["sys-a", makeSystemStatus({ systemId: "sys-a" })],
|
|
419
|
+
["sys-b", makeSystemStatus({ systemId: "sys-b" })],
|
|
420
|
+
["sys-c", makeSystemStatus({ systemId: "sys-c" })],
|
|
421
|
+
["sys-d", makeSystemStatus({ systemId: "sys-d", status: "down" })],
|
|
422
|
+
]),
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
expect(result.size).toBe(2);
|
|
426
|
+
expect(result.get("sys-a")!.derivedState).toBe("down");
|
|
427
|
+
expect(result.get("sys-b")!.derivedState).toBe("degraded");
|
|
428
|
+
expect(result.has("sys-c")).toBe(false);
|
|
429
|
+
});
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
describe("mixed transitive/non-transitive edges", () => {
|
|
433
|
+
// Topology: A --transitive--> B --non-transitive--> C (down)
|
|
434
|
+
// B is operational but depends on C (non-transitively).
|
|
435
|
+
// The non-transitive B→C edge means B only sees C's direct status.
|
|
436
|
+
// Since A→B is transitive, A recursively evaluates B's deps.
|
|
437
|
+
// B→C fires because C is down, promoting B's effective status.
|
|
438
|
+
// A then sees B's promoted status and fires its own warning.
|
|
439
|
+
test("transitive first hop + non-transitive second hop: propagation reaches through direct failures", () => {
|
|
440
|
+
const result = service.evaluateWarnings({
|
|
441
|
+
systemIds: ["sys-a"],
|
|
442
|
+
allDependencies: [
|
|
443
|
+
makeDependency({
|
|
444
|
+
sourceSystemId: "sys-a",
|
|
445
|
+
targetSystemId: "sys-b",
|
|
446
|
+
impactType: "critical",
|
|
447
|
+
transitive: true,
|
|
448
|
+
}),
|
|
449
|
+
makeDependency({
|
|
450
|
+
sourceSystemId: "sys-b",
|
|
451
|
+
targetSystemId: "sys-c",
|
|
452
|
+
impactType: "critical",
|
|
453
|
+
transitive: false,
|
|
454
|
+
}),
|
|
455
|
+
],
|
|
456
|
+
systemStatuses: new Map([
|
|
457
|
+
["sys-a", makeSystemStatus({ systemId: "sys-a" })],
|
|
458
|
+
["sys-b", makeSystemStatus({ systemId: "sys-b", status: "operational" })],
|
|
459
|
+
["sys-c", makeSystemStatus({ systemId: "sys-c", status: "down" })],
|
|
460
|
+
]),
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
// A→B is transitive, so A evaluates B's dependencies.
|
|
464
|
+
// B→C (non-transitive) sees C is down, B gets promoted to "down".
|
|
465
|
+
// A→B (critical + down) = down derived state.
|
|
466
|
+
const warning = result.get("sys-a")!;
|
|
467
|
+
expect(warning).toBeDefined();
|
|
468
|
+
expect(warning.derivedState).toBe("down");
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
// Topology: A --non-transitive--> B --transitive--> C --critical--> D (down)
|
|
472
|
+
// A→B is NOT transitive, so A only sees B's direct status.
|
|
473
|
+
// B is operational, so no warning for A — even though B would
|
|
474
|
+
// transitively derive a warning from C→D.
|
|
475
|
+
test("non-transitive first hop blocks all deeper propagation", () => {
|
|
476
|
+
const result = service.evaluateWarnings({
|
|
477
|
+
systemIds: ["sys-a"],
|
|
478
|
+
allDependencies: [
|
|
479
|
+
makeDependency({
|
|
480
|
+
sourceSystemId: "sys-a",
|
|
481
|
+
targetSystemId: "sys-b",
|
|
482
|
+
impactType: "critical",
|
|
483
|
+
transitive: false,
|
|
484
|
+
}),
|
|
485
|
+
makeDependency({
|
|
486
|
+
sourceSystemId: "sys-b",
|
|
487
|
+
targetSystemId: "sys-c",
|
|
488
|
+
impactType: "critical",
|
|
489
|
+
transitive: true,
|
|
490
|
+
}),
|
|
491
|
+
makeDependency({
|
|
492
|
+
sourceSystemId: "sys-c",
|
|
493
|
+
targetSystemId: "sys-d",
|
|
494
|
+
impactType: "critical",
|
|
495
|
+
}),
|
|
496
|
+
],
|
|
497
|
+
systemStatuses: new Map([
|
|
498
|
+
["sys-a", makeSystemStatus({ systemId: "sys-a" })],
|
|
499
|
+
["sys-b", makeSystemStatus({ systemId: "sys-b", status: "operational" })],
|
|
500
|
+
["sys-c", makeSystemStatus({ systemId: "sys-c", status: "operational" })],
|
|
501
|
+
["sys-d", makeSystemStatus({ systemId: "sys-d", status: "down" })],
|
|
502
|
+
]),
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
// A→B is non-transitive. B is operational (own status).
|
|
506
|
+
// A does NOT look through B's dependencies. No warning.
|
|
507
|
+
expect(result.size).toBe(0);
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
// Topology: A has two branches:
|
|
511
|
+
// A --transitive--> B (operational) --> C (down)
|
|
512
|
+
// A --non-transitive--> D (operational) --> E (down)
|
|
513
|
+
// Expected: A gets a warning from the transitive B branch,
|
|
514
|
+
// but NOT from the non-transitive D branch.
|
|
515
|
+
test("fan-out: transitive branch propagates, non-transitive branch does not", () => {
|
|
516
|
+
const result = service.evaluateWarnings({
|
|
517
|
+
systemIds: ["sys-a"],
|
|
518
|
+
allDependencies: [
|
|
519
|
+
// Transitive branch
|
|
520
|
+
makeDependency({
|
|
521
|
+
sourceSystemId: "sys-a",
|
|
522
|
+
targetSystemId: "sys-b",
|
|
523
|
+
impactType: "critical",
|
|
524
|
+
transitive: true,
|
|
525
|
+
}),
|
|
526
|
+
makeDependency({
|
|
527
|
+
sourceSystemId: "sys-b",
|
|
528
|
+
targetSystemId: "sys-c",
|
|
529
|
+
impactType: "critical",
|
|
530
|
+
}),
|
|
531
|
+
// Non-transitive branch
|
|
532
|
+
makeDependency({
|
|
533
|
+
sourceSystemId: "sys-a",
|
|
534
|
+
targetSystemId: "sys-d",
|
|
535
|
+
impactType: "critical",
|
|
536
|
+
transitive: false,
|
|
537
|
+
}),
|
|
538
|
+
makeDependency({
|
|
539
|
+
sourceSystemId: "sys-d",
|
|
540
|
+
targetSystemId: "sys-e",
|
|
541
|
+
impactType: "critical",
|
|
542
|
+
}),
|
|
543
|
+
],
|
|
544
|
+
systemStatuses: new Map([
|
|
545
|
+
["sys-a", makeSystemStatus({ systemId: "sys-a" })],
|
|
546
|
+
["sys-b", makeSystemStatus({ systemId: "sys-b", status: "operational" })],
|
|
547
|
+
["sys-c", makeSystemStatus({ systemId: "sys-c", status: "down" })],
|
|
548
|
+
["sys-d", makeSystemStatus({ systemId: "sys-d", status: "operational" })],
|
|
549
|
+
["sys-e", makeSystemStatus({ systemId: "sys-e", status: "down" })],
|
|
550
|
+
]),
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
const warning = result.get("sys-a")!;
|
|
554
|
+
expect(warning).toBeDefined();
|
|
555
|
+
expect(warning.derivedState).toBe("down");
|
|
556
|
+
// Only the transitive branch contributes
|
|
557
|
+
expect(warning.affectedUpstreams).toHaveLength(1);
|
|
558
|
+
expect(warning.affectedUpstreams[0].systemId).toBe("sys-b");
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
// Topology: A --transitive--> B --transitive--> C --non-transitive--> D (operational) --> E (down)
|
|
562
|
+
// C is also down directly.
|
|
563
|
+
// Expected: A sees through B and C (both transitive).
|
|
564
|
+
// C→D is non-transitive but C is down regardless.
|
|
565
|
+
// The chain A→B→C propagates C's "down" status all the way.
|
|
566
|
+
test("deep chain: transitive-transitive-nontransitive propagates direct failures", () => {
|
|
567
|
+
const result = service.evaluateWarnings({
|
|
568
|
+
systemIds: ["sys-a"],
|
|
569
|
+
allDependencies: [
|
|
570
|
+
makeDependency({
|
|
571
|
+
sourceSystemId: "sys-a",
|
|
572
|
+
targetSystemId: "sys-b",
|
|
573
|
+
impactType: "critical",
|
|
574
|
+
transitive: true,
|
|
575
|
+
}),
|
|
576
|
+
makeDependency({
|
|
577
|
+
sourceSystemId: "sys-b",
|
|
578
|
+
targetSystemId: "sys-c",
|
|
579
|
+
impactType: "critical",
|
|
580
|
+
transitive: true,
|
|
581
|
+
}),
|
|
582
|
+
makeDependency({
|
|
583
|
+
sourceSystemId: "sys-c",
|
|
584
|
+
targetSystemId: "sys-d",
|
|
585
|
+
impactType: "critical",
|
|
586
|
+
transitive: false,
|
|
587
|
+
}),
|
|
588
|
+
],
|
|
589
|
+
systemStatuses: new Map([
|
|
590
|
+
["sys-a", makeSystemStatus({ systemId: "sys-a" })],
|
|
591
|
+
["sys-b", makeSystemStatus({ systemId: "sys-b", status: "operational" })],
|
|
592
|
+
["sys-c", makeSystemStatus({ systemId: "sys-c", status: "down" })],
|
|
593
|
+
["sys-d", makeSystemStatus({ systemId: "sys-d", status: "operational" })],
|
|
594
|
+
]),
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
const warning = result.get("sys-a")!;
|
|
598
|
+
expect(warning).toBeDefined();
|
|
599
|
+
// C is down → B promoted to down (transitive) → A gets critical+down = down
|
|
600
|
+
expect(warning.derivedState).toBe("down");
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
// Same deep chain but upstream C is operational, failure is only at D.
|
|
604
|
+
// C→D is non-transitive, so C doesn't look through D's deps.
|
|
605
|
+
// But D is directly down and C's edge to D fires.
|
|
606
|
+
// B→C is transitive so B evaluates C's deps.
|
|
607
|
+
// C→D fires (D is down, non-transitive), promoting C to "down".
|
|
608
|
+
// A→B is transitive, so A evaluates B's deps.
|
|
609
|
+
// B→C fires with C promoted to "down". B promoted to "down".
|
|
610
|
+
test("deep chain: non-transitive leaf edge still fires on direct failure", () => {
|
|
611
|
+
const result = service.evaluateWarnings({
|
|
612
|
+
systemIds: ["sys-a"],
|
|
613
|
+
allDependencies: [
|
|
614
|
+
makeDependency({
|
|
615
|
+
sourceSystemId: "sys-a",
|
|
616
|
+
targetSystemId: "sys-b",
|
|
617
|
+
impactType: "critical",
|
|
618
|
+
transitive: true,
|
|
619
|
+
}),
|
|
620
|
+
makeDependency({
|
|
621
|
+
sourceSystemId: "sys-b",
|
|
622
|
+
targetSystemId: "sys-c",
|
|
623
|
+
impactType: "degraded",
|
|
624
|
+
transitive: true,
|
|
625
|
+
}),
|
|
626
|
+
makeDependency({
|
|
627
|
+
sourceSystemId: "sys-c",
|
|
628
|
+
targetSystemId: "sys-d",
|
|
629
|
+
impactType: "critical",
|
|
630
|
+
transitive: false,
|
|
631
|
+
}),
|
|
632
|
+
],
|
|
633
|
+
systemStatuses: new Map([
|
|
634
|
+
["sys-a", makeSystemStatus({ systemId: "sys-a" })],
|
|
635
|
+
["sys-b", makeSystemStatus({ systemId: "sys-b", status: "operational" })],
|
|
636
|
+
["sys-c", makeSystemStatus({ systemId: "sys-c", status: "operational" })],
|
|
637
|
+
["sys-d", makeSystemStatus({ systemId: "sys-d", status: "down" })],
|
|
638
|
+
]),
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
const warning = result.get("sys-a")!;
|
|
642
|
+
expect(warning).toBeDefined();
|
|
643
|
+
// C→D: critical + down = down → C promoted to "down"
|
|
644
|
+
// B→C: degraded + down(promoted) = degraded → B promoted to "degraded"
|
|
645
|
+
// A→B: critical + degraded(promoted) = degraded
|
|
646
|
+
expect(warning.derivedState).toBe("degraded");
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
// Topology: A has two branches with different impact types:
|
|
650
|
+
// A --transitive/informational--> B (operational) --> C (down)
|
|
651
|
+
// A --transitive/critical--------> D (operational) --> E (degraded)
|
|
652
|
+
// Expected: worst-of-all branches applies.
|
|
653
|
+
test("mixed impact fan-out: worst derived state wins across branches", () => {
|
|
654
|
+
const result = service.evaluateWarnings({
|
|
655
|
+
systemIds: ["sys-a"],
|
|
656
|
+
allDependencies: [
|
|
657
|
+
makeDependency({
|
|
658
|
+
sourceSystemId: "sys-a",
|
|
659
|
+
targetSystemId: "sys-b",
|
|
660
|
+
impactType: "informational",
|
|
661
|
+
transitive: true,
|
|
662
|
+
}),
|
|
663
|
+
makeDependency({
|
|
664
|
+
sourceSystemId: "sys-b",
|
|
665
|
+
targetSystemId: "sys-c",
|
|
666
|
+
impactType: "critical",
|
|
667
|
+
}),
|
|
668
|
+
makeDependency({
|
|
669
|
+
sourceSystemId: "sys-a",
|
|
670
|
+
targetSystemId: "sys-d",
|
|
671
|
+
impactType: "critical",
|
|
672
|
+
transitive: true,
|
|
673
|
+
}),
|
|
674
|
+
makeDependency({
|
|
675
|
+
sourceSystemId: "sys-d",
|
|
676
|
+
targetSystemId: "sys-e",
|
|
677
|
+
impactType: "critical",
|
|
678
|
+
}),
|
|
679
|
+
],
|
|
680
|
+
systemStatuses: new Map([
|
|
681
|
+
["sys-a", makeSystemStatus({ systemId: "sys-a" })],
|
|
682
|
+
["sys-b", makeSystemStatus({ systemId: "sys-b", status: "operational" })],
|
|
683
|
+
["sys-c", makeSystemStatus({ systemId: "sys-c", status: "down" })],
|
|
684
|
+
["sys-d", makeSystemStatus({ systemId: "sys-d", status: "operational" })],
|
|
685
|
+
["sys-e", makeSystemStatus({ systemId: "sys-e", status: "degraded" })],
|
|
686
|
+
]),
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
const warning = result.get("sys-a")!;
|
|
690
|
+
expect(warning).toBeDefined();
|
|
691
|
+
// Branch 1: B promoted to "down" from C, then A→B (informational+down) = info
|
|
692
|
+
// Branch 2: D promoted to "degraded" from E, then A→D (critical+degraded) = degraded
|
|
693
|
+
// Worst: degraded > info → degraded
|
|
694
|
+
expect(warning.derivedState).toBe("degraded");
|
|
695
|
+
expect(warning.affectedUpstreams).toHaveLength(2);
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
// All hops operational except last — non-transitive first hop should block everything.
|
|
699
|
+
test("non-transitive first hop shields from deep transitive chain failure", () => {
|
|
700
|
+
const result = service.evaluateWarnings({
|
|
701
|
+
systemIds: ["sys-a"],
|
|
702
|
+
allDependencies: [
|
|
703
|
+
makeDependency({
|
|
704
|
+
sourceSystemId: "sys-a",
|
|
705
|
+
targetSystemId: "sys-b",
|
|
706
|
+
impactType: "critical",
|
|
707
|
+
transitive: false,
|
|
708
|
+
}),
|
|
709
|
+
makeDependency({
|
|
710
|
+
sourceSystemId: "sys-b",
|
|
711
|
+
targetSystemId: "sys-c",
|
|
712
|
+
impactType: "critical",
|
|
713
|
+
transitive: true,
|
|
714
|
+
}),
|
|
715
|
+
makeDependency({
|
|
716
|
+
sourceSystemId: "sys-c",
|
|
717
|
+
targetSystemId: "sys-d",
|
|
718
|
+
impactType: "critical",
|
|
719
|
+
transitive: true,
|
|
720
|
+
}),
|
|
721
|
+
makeDependency({
|
|
722
|
+
sourceSystemId: "sys-d",
|
|
723
|
+
targetSystemId: "sys-e",
|
|
724
|
+
impactType: "critical",
|
|
725
|
+
transitive: true,
|
|
726
|
+
}),
|
|
727
|
+
],
|
|
728
|
+
systemStatuses: new Map([
|
|
729
|
+
["sys-a", makeSystemStatus({ systemId: "sys-a" })],
|
|
730
|
+
["sys-b", makeSystemStatus({ systemId: "sys-b", status: "operational" })],
|
|
731
|
+
["sys-c", makeSystemStatus({ systemId: "sys-c", status: "operational" })],
|
|
732
|
+
["sys-d", makeSystemStatus({ systemId: "sys-d", status: "operational" })],
|
|
733
|
+
["sys-e", makeSystemStatus({ systemId: "sys-e", status: "down" })],
|
|
734
|
+
]),
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
// A→B is non-transitive. B is operational. A sees no failure.
|
|
738
|
+
expect(result.size).toBe(0);
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
// Verify that the intermediate system B is correctly warned even when A is not.
|
|
742
|
+
test("intermediate system gets warning even when downstream is shielded", () => {
|
|
743
|
+
const result = service.evaluateWarnings({
|
|
744
|
+
systemIds: ["sys-a", "sys-b"],
|
|
745
|
+
allDependencies: [
|
|
746
|
+
makeDependency({
|
|
747
|
+
sourceSystemId: "sys-a",
|
|
748
|
+
targetSystemId: "sys-b",
|
|
749
|
+
impactType: "critical",
|
|
750
|
+
transitive: false,
|
|
751
|
+
}),
|
|
752
|
+
makeDependency({
|
|
753
|
+
sourceSystemId: "sys-b",
|
|
754
|
+
targetSystemId: "sys-c",
|
|
755
|
+
impactType: "critical",
|
|
756
|
+
transitive: false,
|
|
757
|
+
}),
|
|
758
|
+
],
|
|
759
|
+
systemStatuses: new Map([
|
|
760
|
+
["sys-a", makeSystemStatus({ systemId: "sys-a" })],
|
|
761
|
+
["sys-b", makeSystemStatus({ systemId: "sys-b", status: "operational" })],
|
|
762
|
+
["sys-c", makeSystemStatus({ systemId: "sys-c", status: "down" })],
|
|
763
|
+
]),
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
// B→C fires (C is down). B gets warning "down".
|
|
767
|
+
// A→B is non-transitive and B is operational (own status). A has no warning.
|
|
768
|
+
expect(result.size).toBe(1);
|
|
769
|
+
expect(result.has("sys-a")).toBe(false);
|
|
770
|
+
expect(result.get("sys-b")!.derivedState).toBe("down");
|
|
771
|
+
});
|
|
772
|
+
});
|
|
773
|
+
});
|