@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.
@@ -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
+ });