@skyhook-io/radar-app 1.13.3 → 1.13.4

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.
@@ -9,6 +9,7 @@ import {
9
9
  projectInvestigationEvidence,
10
10
  resolveInvestigationRootCauseEvidence,
11
11
  type InvestigationEvidenceGroup,
12
+ type InvestigationEvidenceTarget,
12
13
  type InvestigationEvidenceTimelineItem,
13
14
  type InvestigationEvidenceTurn,
14
15
  } from "./investigationEvidence";
@@ -943,7 +944,9 @@ describe("strict evidence adapters", () => {
943
944
  ]);
944
945
  const events = groupsOf(result.groups, "events")[0].latest;
945
946
 
946
- expect(events.summary).toBe("1 event group · Deployment shop/api");
947
+ expect(events.summary).toBe(
948
+ "BackOff: Back-off restarting failed container · Deployment shop/api",
949
+ );
947
950
  expect(events.data).toMatchObject({
948
951
  type: "events",
949
952
  scope: "Deployment shop/api",
@@ -2815,7 +2818,8 @@ describe("honest zero and partial-result states", () => {
2815
2818
  );
2816
2819
  // list_resources uses [] for some RBAC-filtered reads, so its emptiness is
2817
2820
  // never a positive receipt even when the transport itself succeeded. The
2818
- // narrow event/change producers have the same ambiguity today; the targeted
2821
+ // events producer marks a denied namespace explicitly, so its empty result
2822
+ // is a receipt; the changes producer still has the ambiguity. The targeted
2819
2823
  // semantic diagnose bundle is authoritative because access is checked before
2820
2824
  // that workload path runs.
2821
2825
  expect(
@@ -2826,16 +2830,15 @@ describe("honest zero and partial-result states", () => {
2826
2830
  ),
2827
2831
  ).toBe(false);
2828
2832
  expect(result.limitations.map((item) => item.source)).toEqual(
2829
- expect.arrayContaining([
2830
- "Events",
2831
- "Recent changes",
2832
- "Resource inventory",
2833
- ]),
2833
+ expect.arrayContaining(["Recent changes", "Resource inventory"]),
2834
+ );
2835
+ expect(result.limitations.map((item) => item.source)).not.toContain(
2836
+ "Events",
2834
2837
  );
2835
2838
  expect(result.coverage).toEqual({
2836
2839
  attempted: 5,
2837
- projected: 2,
2838
- limited: 3,
2840
+ projected: 3,
2841
+ limited: 2,
2839
2842
  checked: 1,
2840
2843
  });
2841
2844
  expect(
@@ -3107,3 +3110,2093 @@ describe("honest zero and partial-result states", () => {
3107
3110
  ]);
3108
3111
  });
3109
3112
  });
3113
+
3114
+ // The get_workload_logs response shape, with the stream rows fetchPodLogs
3115
+ // emits per pod and container.
3116
+ const workloadLogsPayload = {
3117
+ workload: "deployments/shop/api",
3118
+ pods: 2,
3119
+ logs: [
3120
+ {
3121
+ pod: "api-68c7b766dc-fmphn",
3122
+ container: "api",
3123
+ logs: {
3124
+ lines: [
3125
+ "2026-09-07T08:00:20.234687659Z [Nest] 1 - 09/07/2026, 8:00:20 AM  ERROR [MongooseModule] Unable to connect to the database. Retrying (9)...",
3126
+ "2026-09-07T08:00:20.234756499Z MongoServerError: Authentication failed.",
3127
+ ],
3128
+ totalLines: 50,
3129
+ matchedLines: 4,
3130
+ fallback: false,
3131
+ },
3132
+ },
3133
+ {
3134
+ pod: "api-68c7b766dc-fmphn",
3135
+ container: "istio-proxy",
3136
+ logs: {
3137
+ lines: ["2026-09-07T08:00:19Z info Envoy proxy is ready"],
3138
+ totalLines: 12,
3139
+ matchedLines: 0,
3140
+ fallback: true,
3141
+ },
3142
+ },
3143
+ {
3144
+ pod: "api-68c7b766dc-q2zxr",
3145
+ container: "api",
3146
+ logs: {
3147
+ lines: [
3148
+ "2026-09-07T08:00:21Z MongoServerError: Authentication failed.",
3149
+ ],
3150
+ totalLines: 50,
3151
+ matchedLines: 1,
3152
+ fallback: false,
3153
+ },
3154
+ },
3155
+ ],
3156
+ narrowHint:
3157
+ "at least one pod's log stream tailed to 50 lines (cap reached) — narrow with since= (e.g. 10m), grep= regex, container=, or raise tail_lines",
3158
+ warnings: [
3159
+ "1 of 2 pod(s) have container restarts on record; the error(s) that killed prior containers are in the previous instance's logs — call again with `previous: true` to see them.",
3160
+ ],
3161
+ };
3162
+
3163
+ const workloadLogsArgs = JSON.stringify({
3164
+ namespace: "shop",
3165
+ name: "api",
3166
+ kind: "deployment",
3167
+ tail_lines: 50,
3168
+ });
3169
+
3170
+ describe("workload logs adapter", () => {
3171
+ it("keeps one multi-container result as one source with several observations", () => {
3172
+ const ref = evidenceRef("a", "b");
3173
+ const projection = project([
3174
+ tool("wl-logs", "get_workload_logs", workloadLogsPayload, {
3175
+ summary: workloadLogsArgs,
3176
+ evidenceRef: ref,
3177
+ }),
3178
+ ]);
3179
+ const logs = groupsOf(projection.groups, "logs");
3180
+ expect(logs.map((group) => group.identity)).toEqual([
3181
+ "logs:current:api-68c7b766dc-fmphn:api",
3182
+ "logs:current:api-68c7b766dc-fmphn:istio-proxy",
3183
+ "logs:current:api-68c7b766dc-q2zxr:api",
3184
+ ]);
3185
+ expect(projection.sources).toHaveLength(1);
3186
+ expect(projection.coverage).toMatchObject({
3187
+ attempted: 1,
3188
+ projected: 1,
3189
+ limited: 1,
3190
+ });
3191
+ expect(new Set(logs.map((group) => group.latest.source.id)).size).toBe(1);
3192
+ // Diagnose relates a target workload to its pods the same way.
3193
+ expect(logs.map((group) => group.latest.relevance)).toEqual([
3194
+ "producer-related",
3195
+ "producer-related",
3196
+ "producer-related",
3197
+ ]);
3198
+ expect(logs[0].latest.tier).toBe("supporting");
3199
+ expect(logs[0].latest.tone).toBe("warning");
3200
+ expect(logs[0].latest.data).toMatchObject({
3201
+ type: "logs",
3202
+ namespace: "shop",
3203
+ previous: false,
3204
+ warnings: workloadLogsPayload.warnings,
3205
+ });
3206
+ expect(
3207
+ (logs[0].latest.data as { logs?: { lines: string[] } }).logs?.lines[0],
3208
+ ).not.toContain("[");
3209
+ expect(logs[1].latest.tier).toBe("context");
3210
+ expect(investigationEvidenceSubjectRef(logs[2].latest.data)).toEqual({
3211
+ kind: "Pod",
3212
+ namespace: "shop",
3213
+ name: "api-68c7b766dc-q2zxr",
3214
+ });
3215
+
3216
+ const resolution = resolveInvestigationRootCauseEvidence(
3217
+ projection,
3218
+ { status: "linked", refs: [ref] },
3219
+ 0,
3220
+ );
3221
+ expect(resolution.status).toBe("linked");
3222
+ expect(resolution.links[0].source.stepId).toBe("wl-logs");
3223
+ expect(resolution.links[0].originalGroupId).toBe(logs[0].id);
3224
+ expect(projection.sources[0].primaryGroupId).toBe(logs[0].id);
3225
+ expect(
3226
+ projection.limitations.map((limitation) => limitation.source),
3227
+ ).toEqual(["Workload logs"]);
3228
+ expect(projection.limitations[0].kind).toBe("truncated");
3229
+ });
3230
+
3231
+ it("accepts every workload kind the producer resolves, including Rollouts", () => {
3232
+ const rollout = projectInvestigationEvidence(
3233
+ [
3234
+ {
3235
+ timeline: [
3236
+ tool(
3237
+ "wl-rollout",
3238
+ "get_workload_logs",
3239
+ { ...workloadLogsPayload, workload: "rollouts/shop/api" },
3240
+ {
3241
+ summary: JSON.stringify({
3242
+ namespace: "shop",
3243
+ name: "api",
3244
+ kind: "rollout",
3245
+ }),
3246
+ },
3247
+ ),
3248
+ ],
3249
+ },
3250
+ ],
3251
+ { kind: "Rollout", group: "argoproj.io", namespace: "shop", name: "api" },
3252
+ );
3253
+ expect(
3254
+ groupsOf(rollout.groups, "logs").map((group) => group.latest.relevance),
3255
+ ).toEqual(["producer-related", "producer-related", "producer-related"]);
3256
+ expect(rollout.limitations.map((limitation) => limitation.kind)).toEqual([
3257
+ "truncated",
3258
+ ]);
3259
+ });
3260
+
3261
+ it("scopes a sibling workload's logs as broader and a Pod target's own row as target", () => {
3262
+ const sibling = project([
3263
+ tool(
3264
+ "wl-sibling",
3265
+ "get_workload_logs",
3266
+ { ...workloadLogsPayload, workload: "deployments/shop/api-worker" },
3267
+ {
3268
+ summary: JSON.stringify({
3269
+ namespace: "shop",
3270
+ name: "api-worker",
3271
+ kind: "deployment",
3272
+ }),
3273
+ },
3274
+ ),
3275
+ ]);
3276
+ expect(
3277
+ groupsOf(sibling.groups, "logs").map((group) => group.latest.relevance),
3278
+ ).toEqual(["broader", "broader", "broader"]);
3279
+ expect(
3280
+ groupsOf(sibling.groups, "logs").map((group) => group.latest.tier),
3281
+ ).toEqual(["context", "context", "context"]);
3282
+
3283
+ const podTarget = projectInvestigationEvidence(
3284
+ [
3285
+ {
3286
+ timeline: [
3287
+ tool("wl-pod", "get_workload_logs", workloadLogsPayload, {
3288
+ summary: workloadLogsArgs,
3289
+ }),
3290
+ ],
3291
+ },
3292
+ ],
3293
+ {
3294
+ kind: "Pod",
3295
+ group: "",
3296
+ namespace: "shop",
3297
+ name: "api-68c7b766dc-q2zxr",
3298
+ },
3299
+ );
3300
+ expect(
3301
+ groupsOf(podTarget.groups, "logs").map((group) => group.latest.relevance),
3302
+ ).toEqual(["broader", "broader", "target"]);
3303
+ });
3304
+
3305
+ it("reports a failed read as a limitation and no pods as a scoped receipt", () => {
3306
+ const failed = project([
3307
+ tool(
3308
+ "wl-error",
3309
+ "get_workload_logs",
3310
+ {
3311
+ workload: "deployments/shop/api",
3312
+ pods: 2,
3313
+ logsError: "no kube client in request context",
3314
+ },
3315
+ { summary: workloadLogsArgs },
3316
+ ),
3317
+ ]);
3318
+ expect(failed.groups).toHaveLength(0);
3319
+ expect(failed.limitations).toEqual([
3320
+ expect.objectContaining({
3321
+ source: "Workload logs",
3322
+ message: "no kube client in request context",
3323
+ kind: "error",
3324
+ }),
3325
+ ]);
3326
+
3327
+ const empty = project([
3328
+ tool(
3329
+ "wl-empty",
3330
+ "get_workload_logs",
3331
+ {
3332
+ workload: "jobs/shop/api",
3333
+ pods: 0,
3334
+ logs: "No pods found for this Job yet. Check scheduling, admission, or controller events.",
3335
+ emptyReason: "no-pods",
3336
+ emptyMessage:
3337
+ "No pods found for this Job yet. Check scheduling, admission, or controller events.",
3338
+ command: "kubectl logs job/api -n shop",
3339
+ },
3340
+ {
3341
+ summary: JSON.stringify({
3342
+ namespace: "shop",
3343
+ name: "api",
3344
+ kind: "job",
3345
+ }),
3346
+ },
3347
+ ),
3348
+ ]);
3349
+ const receipt = groupsOf(empty.groups, "receipt")[0].latest;
3350
+ expect(receipt.tier).toBe("context");
3351
+ expect(receipt.relevance).toBe("broader");
3352
+ expect(receipt.data).toMatchObject({
3353
+ type: "receipt",
3354
+ checked: "logs",
3355
+ scope: "Job shop/api",
3356
+ message:
3357
+ "No pods found for this Job yet. Check scheduling, admission, or controller events.",
3358
+ });
3359
+ expect(empty.limitations).toHaveLength(0);
3360
+
3361
+ const unconfirmed = project([
3362
+ tool(
3363
+ "wl-unconfirmed",
3364
+ "get_workload_logs",
3365
+ { workload: "deployments/shop/api", pods: 0, logs: "no pods" },
3366
+ { summary: workloadLogsArgs, isError: undefined },
3367
+ ),
3368
+ ]);
3369
+ expect(groupsOf(unconfirmed.groups, "receipt")).toHaveLength(0);
3370
+ });
3371
+
3372
+ it("rejects malformed payloads and per-stream errors honestly", () => {
3373
+ const malformed = project([
3374
+ tool(
3375
+ "wl-bad",
3376
+ "get_workload_logs",
3377
+ { workload: "pods/shop/api", pods: 1, logs: [] },
3378
+ { summary: workloadLogsArgs },
3379
+ ),
3380
+ tool(
3381
+ "wl-stream-error",
3382
+ "get_workload_logs",
3383
+ {
3384
+ workload: "deployments/shop/api",
3385
+ pods: 1,
3386
+ logs: [
3387
+ {
3388
+ pod: "api-68c7b766dc-fmphn",
3389
+ container: "api",
3390
+ error: "failed to get logs: container is waiting to start",
3391
+ },
3392
+ ],
3393
+ },
3394
+ { summary: workloadLogsArgs },
3395
+ ),
3396
+ tool(
3397
+ "wl-null",
3398
+ "get_workload_logs",
3399
+ { workload: "deployments/shop/api", pods: 3, logs: null },
3400
+ { summary: workloadLogsArgs },
3401
+ ),
3402
+ tool(
3403
+ "wl-zero-with-streams",
3404
+ "get_workload_logs",
3405
+ {
3406
+ workload: "deployments/shop/api",
3407
+ pods: 0,
3408
+ logs: workloadLogsPayload.logs,
3409
+ },
3410
+ { summary: workloadLogsArgs },
3411
+ ),
3412
+ ]);
3413
+ expect(malformed.groups).toHaveLength(0);
3414
+ expect(
3415
+ malformed.limitations.map((limitation) => [
3416
+ limitation.source,
3417
+ limitation.kind,
3418
+ ]),
3419
+ ).toEqual([
3420
+ ["Workload logs", "unknown"],
3421
+ ["api-68c7b766dc-fmphn / api", "error"],
3422
+ ["Workload logs", "unknown"],
3423
+ ]);
3424
+ expect(malformed.limitations[0].sources.map((item) => item.stepId)).toEqual(
3425
+ ["wl-bad", "wl-zero-with-streams"],
3426
+ );
3427
+ expect(malformed.limitations[0].message).toContain(
3428
+ "couldn't summarize this investigation step",
3429
+ );
3430
+ expect(malformed.limitations[2].message).toContain("3 resolved pods");
3431
+ });
3432
+ });
3433
+
3434
+ const firingInstance = {
3435
+ state: "firing",
3436
+ activeAt: "2026-09-07T07:58:00Z",
3437
+ value: "1e+00",
3438
+ labels: {
3439
+ alertname: "KubePodCrashLooping",
3440
+ namespace: "shop",
3441
+ pod: "api-68c7b766dc-fmphn",
3442
+ container: "api",
3443
+ severity: "warning",
3444
+ },
3445
+ };
3446
+
3447
+ function alertingRule(
3448
+ patch: Record<string, unknown> = {},
3449
+ ): Record<string, unknown> {
3450
+ return {
3451
+ group: "kubernetes-apps",
3452
+ type: "alerting",
3453
+ name: "KubePodCrashLooping",
3454
+ query:
3455
+ 'max_over_time(kube_pod_container_status_waiting_reason{reason="CrashLoopBackOff"}[5m]) >= 1',
3456
+ state: "firing",
3457
+ health: "ok",
3458
+ labels: { severity: "warning" },
3459
+ annotations: {
3460
+ summary: "Pod is crash looping.",
3461
+ description:
3462
+ "Pod {{ $labels.namespace }}/{{ $labels.pod }} ({{ $labels.container }}) is in waiting state (reason: CrashLoopBackOff).",
3463
+ },
3464
+ alerts: [firingInstance],
3465
+ ...patch,
3466
+ };
3467
+ }
3468
+
3469
+ const rulesArgs = JSON.stringify({ state: "firing" });
3470
+
3471
+ /** A diagnose read that ties the given pods to the target as its log streams. */
3472
+ function diagnoseWithPods(...pods: string[]) {
3473
+ return tool("diagnose-pods", "diagnose", {
3474
+ resource: deployment,
3475
+ resourceContext: { tier: "basic" },
3476
+ logsCurrent: pods.map((pod) => ({
3477
+ pod,
3478
+ container: "api",
3479
+ logs: {
3480
+ lines: ["ready"],
3481
+ totalLines: 1,
3482
+ matchedLines: 0,
3483
+ fallback: true,
3484
+ },
3485
+ })),
3486
+ });
3487
+ }
3488
+
3489
+ describe("prometheus rules adapter", () => {
3490
+ it("relates a rule to the target through its instances, never the rule alone", () => {
3491
+ const projection = project([
3492
+ diagnoseWithPods("api-68c7b766dc-fmphn"),
3493
+ tool(
3494
+ "rules",
3495
+ "get_prometheus_rules",
3496
+ {
3497
+ count: 4,
3498
+ rules: [
3499
+ alertingRule(),
3500
+ alertingRule({
3501
+ name: "KubeDeploymentReplicasMismatch",
3502
+ labels: { severity: "warning", namespace: "shop" },
3503
+ alerts: [
3504
+ {
3505
+ state: "firing",
3506
+ labels: {
3507
+ namespace: "shop",
3508
+ deployment: "api-worker",
3509
+ },
3510
+ },
3511
+ ],
3512
+ }),
3513
+ alertingRule({
3514
+ name: "TargetDown",
3515
+ group: "general",
3516
+ labels: { severity: "critical" },
3517
+ alerts: [
3518
+ {
3519
+ state: "firing",
3520
+ labels: { namespace: "monitoring", job: "node-exporter" },
3521
+ },
3522
+ ],
3523
+ }),
3524
+ alertingRule({
3525
+ name: "SiblingCrash",
3526
+ alerts: [
3527
+ {
3528
+ state: "firing",
3529
+ labels: {
3530
+ namespace: "shop",
3531
+ pod: "api-worker-5d8f7c9b6-x2k9p",
3532
+ },
3533
+ },
3534
+ ],
3535
+ }),
3536
+ ],
3537
+ },
3538
+ { summary: rulesArgs },
3539
+ ),
3540
+ ]);
3541
+ const alerts = groupsOf(projection.groups, "alerts");
3542
+ expect(alerts.map((group) => group.latest.relevance)).toEqual([
3543
+ "target",
3544
+ "broader",
3545
+ "broader",
3546
+ "broader",
3547
+ ]);
3548
+ expect(alerts.map((group) => group.latest.tier)).toEqual([
3549
+ "supporting",
3550
+ "context",
3551
+ "context",
3552
+ "context",
3553
+ ]);
3554
+ expect(alerts[0].latest.tone).toBe("alert");
3555
+ expect(alerts[2].latest.tone).toBe("error");
3556
+ expect(alerts[0].latest.title).toBe("KubePodCrashLooping firing");
3557
+ expect(alerts[0].latest.summary).toBe(
3558
+ "1 active instance, 1 naming Deployment shop/api · kubernetes-apps",
3559
+ );
3560
+ expect(alerts[0].latest.data).toMatchObject({
3561
+ type: "alerts",
3562
+ rule: { group: "kubernetes-apps", state: "firing", health: "ok" },
3563
+ instances: [{ namesTarget: true, value: "1e+00" }],
3564
+ annotations: { summary: "Pod is crash looping." },
3565
+ });
3566
+ expect(alerts[3].latest.data).toMatchObject({
3567
+ instances: [{ namesTarget: false }],
3568
+ });
3569
+ expect(
3570
+ groupsOf(projection.groups, "receipt").filter((group) =>
3571
+ group.identity.startsWith("alerts:"),
3572
+ ),
3573
+ ).toHaveLength(0);
3574
+ expect(
3575
+ projection.limitations.filter((limitation) =>
3576
+ limitation.source.startsWith("Alert"),
3577
+ ),
3578
+ ).toHaveLength(0);
3579
+ });
3580
+
3581
+ it("uses rule-level labels only as producer-related context", () => {
3582
+ const projection = project([
3583
+ tool(
3584
+ "rules",
3585
+ "get_prometheus_rules",
3586
+ {
3587
+ count: 1,
3588
+ rules: [
3589
+ alertingRule({
3590
+ name: "ApiDown",
3591
+ labels: {
3592
+ severity: "warning",
3593
+ namespace: "shop",
3594
+ deployment: "api",
3595
+ },
3596
+ alerts: [
3597
+ {
3598
+ state: "firing",
3599
+ labels: { namespace: "shop", instance: "10.0.0.4:8080" },
3600
+ },
3601
+ ],
3602
+ }),
3603
+ ],
3604
+ },
3605
+ { summary: rulesArgs },
3606
+ ),
3607
+ ]);
3608
+ const alert = groupsOf(projection.groups, "alerts")[0].latest;
3609
+ expect(alert.relevance).toBe("producer-related");
3610
+ expect(alert.tier).toBe("supporting");
3611
+ expect(alert.summary).toBe("1 active instance · kubernetes-apps");
3612
+ expect(groupsOf(projection.groups, "receipt")).toHaveLength(0);
3613
+ });
3614
+
3615
+ it("matches workload labels per kind and pod labels only through producer-established pods", () => {
3616
+ const run = (
3617
+ target: InvestigationEvidenceTarget,
3618
+ labels: object,
3619
+ capturedPods: string[] = [],
3620
+ ) =>
3621
+ groupsOf(
3622
+ projectInvestigationEvidence(
3623
+ [
3624
+ {
3625
+ timeline: [
3626
+ ...(capturedPods.length > 0
3627
+ ? [
3628
+ tool("diagnose-pods", "diagnose", {
3629
+ resource: {
3630
+ ...deployment,
3631
+ kind: target.kind,
3632
+ metadata: {
3633
+ namespace: target.namespace,
3634
+ name: target.name,
3635
+ },
3636
+ },
3637
+ resourceContext: { tier: "basic" },
3638
+ logsCurrent: capturedPods.map((pod) => ({
3639
+ pod,
3640
+ container: "app",
3641
+ logs: {
3642
+ lines: ["ready"],
3643
+ totalLines: 1,
3644
+ matchedLines: 0,
3645
+ fallback: true,
3646
+ },
3647
+ })),
3648
+ }),
3649
+ ]
3650
+ : []),
3651
+ tool(
3652
+ "rules",
3653
+ "get_prometheus_rules",
3654
+ {
3655
+ count: 1,
3656
+ rules: [
3657
+ alertingRule({ alerts: [{ state: "firing", labels }] }),
3658
+ ],
3659
+ },
3660
+ { summary: rulesArgs },
3661
+ ),
3662
+ ],
3663
+ },
3664
+ ],
3665
+ target,
3666
+ ).groups,
3667
+ "alerts",
3668
+ )[0].latest.relevance;
3669
+ const statefulSet = { ...target, kind: "StatefulSet", name: "db" };
3670
+ expect(run(target, { namespace: "shop", deployment: "api" })).toBe(
3671
+ "target",
3672
+ );
3673
+ expect(run(target, { namespace: "other", deployment: "api" })).toBe(
3674
+ "broader",
3675
+ );
3676
+ expect(run(target, { namespace: "shop", statefulset: "api" })).toBe(
3677
+ "broader",
3678
+ );
3679
+ expect(
3680
+ run(target, {
3681
+ namespace: "shop",
3682
+ workload: "api",
3683
+ workload_type: "deployment",
3684
+ }),
3685
+ ).toBe("target");
3686
+ expect(
3687
+ run(target, {
3688
+ namespace: "shop",
3689
+ workload: "api",
3690
+ workload_type: "statefulset",
3691
+ }),
3692
+ ).toBe("broader");
3693
+ // A controller-shaped pod name proves nothing on its own: a sibling
3694
+ // ReplicaSet `api-worker` produces `api-worker-abcde` too.
3695
+ expect(
3696
+ run(target, { namespace: "shop", pod: "api-68c7b766dc-fmphn" }),
3697
+ ).toBe("broader");
3698
+ expect(
3699
+ run(target, { namespace: "shop", pod: "api-68c7b766dc-fmphn" }, [
3700
+ "api-68c7b766dc-fmphn",
3701
+ ]),
3702
+ ).toBe("target");
3703
+ expect(
3704
+ run(target, { namespace: "shop", pod: "api-worker-abcde" }, [
3705
+ "api-68c7b766dc-fmphn",
3706
+ ]),
3707
+ ).toBe("broader");
3708
+ expect(run(statefulSet, { namespace: "shop", pod: "db-0" }, ["db-0"])).toBe(
3709
+ "target",
3710
+ );
3711
+ expect(
3712
+ run(statefulSet, { namespace: "other", pod: "db-0" }, ["db-0"]),
3713
+ ).toBe("broader");
3714
+ expect(
3715
+ run(target, { pod: "api-68c7b766dc-fmphn" }, ["api-68c7b766dc-fmphn"]),
3716
+ ).toBe("broader");
3717
+ });
3718
+
3719
+ it("issues a scoped receipt only for a complete, unfiltered, evaluated active-state query", () => {
3720
+ const unrelated = alertingRule({
3721
+ name: "TargetDown",
3722
+ alerts: [{ state: "firing", labels: { namespace: "monitoring" } }],
3723
+ });
3724
+ const receipt = (
3725
+ payload: Record<string, unknown>,
3726
+ args: Record<string, unknown>,
3727
+ ) =>
3728
+ groupsOf(
3729
+ project([
3730
+ tool("rules", "get_prometheus_rules", payload, {
3731
+ summary: JSON.stringify(args),
3732
+ }),
3733
+ ]).groups,
3734
+ "receipt",
3735
+ );
3736
+
3737
+ const firing = receipt(
3738
+ { count: 1, rules: [unrelated] },
3739
+ { state: "firing" },
3740
+ );
3741
+ expect(firing).toHaveLength(1);
3742
+ expect(firing[0].latest).toMatchObject({
3743
+ tier: "checked",
3744
+ relevance: "target",
3745
+ title: "No firing alert rules name this Deployment",
3746
+ summary: "Deployment shop/api",
3747
+ data: {
3748
+ type: "receipt",
3749
+ checked: "alerts",
3750
+ scope: "Deployment shop/api",
3751
+ message:
3752
+ "1 alerting rule returned (filter: state=firing), every instance in another namespace; none names Deployment shop/api.",
3753
+ },
3754
+ });
3755
+
3756
+ expect(
3757
+ receipt(
3758
+ {
3759
+ count: 0,
3760
+ rules: [],
3761
+ note: "no rules matched — drop filters to see what exists, or the backend may have no rules configured",
3762
+ },
3763
+ { state: "pending" },
3764
+ )[0].latest.data,
3765
+ ).toMatchObject({
3766
+ message: "0 alerting rules returned (filter: state=pending).",
3767
+ });
3768
+
3769
+ // A name or group filter never looked at the other rules.
3770
+ expect(
3771
+ receipt(
3772
+ { count: 1, rules: [unrelated] },
3773
+ { state: "firing", name: "Target" },
3774
+ ),
3775
+ ).toHaveLength(0);
3776
+ expect(
3777
+ receipt({ count: 0, rules: [] }, { state: "firing", group: "general" }),
3778
+ ).toHaveLength(0);
3779
+ // An instance this projection cannot place is not evidence of absence.
3780
+ expect(
3781
+ receipt(
3782
+ {
3783
+ count: 1,
3784
+ rules: [
3785
+ alertingRule({
3786
+ name: "Vague",
3787
+ alerts: [
3788
+ { state: "firing", labels: { instance: "10.0.0.4:9100" } },
3789
+ ],
3790
+ }),
3791
+ ],
3792
+ },
3793
+ { state: "firing" },
3794
+ ),
3795
+ ).toHaveLength(0);
3796
+ expect(
3797
+ receipt(
3798
+ {
3799
+ count: 1,
3800
+ rules: [
3801
+ alertingRule({
3802
+ name: "SameNamespace",
3803
+ alerts: [
3804
+ {
3805
+ state: "firing",
3806
+ labels: { namespace: "shop", pod: "other-abc" },
3807
+ },
3808
+ ],
3809
+ }),
3810
+ ],
3811
+ },
3812
+ { state: "firing" },
3813
+ ),
3814
+ ).toHaveLength(0);
3815
+ expect(receipt({ count: 1, rules: [unrelated] }, {})).toHaveLength(0);
3816
+ expect(
3817
+ receipt({ count: 1, rules: [unrelated] }, { state: "inactive" }),
3818
+ ).toHaveLength(0);
3819
+ expect(
3820
+ receipt(
3821
+ { count: 1, rules: [alertingRule({ type: "recording", name: "x" })] },
3822
+ { type: "record", state: "firing" },
3823
+ ),
3824
+ ).toHaveLength(0);
3825
+ expect(
3826
+ receipt(
3827
+ {
3828
+ count: 1,
3829
+ rules: [unrelated],
3830
+ truncated: true,
3831
+ note: "narrow with name, group, state, or type filters",
3832
+ },
3833
+ { state: "firing" },
3834
+ ),
3835
+ ).toHaveLength(0);
3836
+ expect(
3837
+ receipt(
3838
+ { count: 1, rules: [{ ...unrelated, health: "err" }] },
3839
+ { state: "firing" },
3840
+ ),
3841
+ ).toHaveLength(0);
3842
+ expect(
3843
+ receipt(
3844
+ { count: 1, rules: [{ ...unrelated, health: "degraded" }] },
3845
+ { state: "firing" },
3846
+ ),
3847
+ ).toHaveLength(0);
3848
+ expect(
3849
+ receipt(
3850
+ { count: 1, rules: [{ ...unrelated, health: undefined }] },
3851
+ { state: "firing" },
3852
+ ),
3853
+ ).toHaveLength(0);
3854
+ });
3855
+
3856
+ it("advances the card through alert state transitions and keeps distinct rules apart", () => {
3857
+ const firingForTarget = alertingRule();
3858
+ const resolved = alertingRule({ state: "inactive", alerts: [] });
3859
+ const firingElsewhere = alertingRule({
3860
+ alerts: [
3861
+ {
3862
+ state: "firing",
3863
+ labels: { namespace: "shop", pod: "api-worker-5d8f7c9b6-x2k9p" },
3864
+ },
3865
+ ],
3866
+ });
3867
+ const run = (...later: Record<string, unknown>[]) =>
3868
+ groupsOf(
3869
+ project(
3870
+ [
3871
+ diagnoseWithPods("api-68c7b766dc-fmphn"),
3872
+ tool(
3873
+ "rules-1",
3874
+ "get_prometheus_rules",
3875
+ { count: 1, rules: [firingForTarget] },
3876
+ { summary: rulesArgs },
3877
+ ),
3878
+ ],
3879
+ [
3880
+ tool(
3881
+ "rules-2",
3882
+ "get_prometheus_rules",
3883
+ { count: later.length, rules: later },
3884
+ { summary: JSON.stringify({}) },
3885
+ ),
3886
+ ],
3887
+ ).groups,
3888
+ "alerts",
3889
+ );
3890
+
3891
+ const [resolvedGroup] = run(resolved);
3892
+ expect(resolvedGroup.observations).toHaveLength(2);
3893
+ expect(resolvedGroup.latest).toMatchObject({
3894
+ title: "KubePodCrashLooping inactive",
3895
+ relevance: "target",
3896
+ tier: "context",
3897
+ tone: "neutral",
3898
+ summary: "No active instances · kubernetes-apps",
3899
+ });
3900
+ expect(resolvedGroup.latest).toBe(resolvedGroup.chronologicalLatest);
3901
+ expect(resolvedGroup.historical).toBe(false);
3902
+
3903
+ const [movedGroup] = run(firingElsewhere);
3904
+ expect(movedGroup.observations).toHaveLength(2);
3905
+ expect(movedGroup.latest.relevance).toBe("broader");
3906
+ expect(movedGroup.latest.summary).toBe(
3907
+ "1 active instance · kubernetes-apps",
3908
+ );
3909
+
3910
+ const distinct = groupsOf(
3911
+ project([
3912
+ tool(
3913
+ "rules",
3914
+ "get_prometheus_rules",
3915
+ {
3916
+ count: 2,
3917
+ rules: [
3918
+ alertingRule({ labels: { severity: "warning", team: "a" } }),
3919
+ alertingRule({
3920
+ query: "vector(1)",
3921
+ labels: { severity: "critical", team: "b" },
3922
+ }),
3923
+ ],
3924
+ },
3925
+ { summary: rulesArgs },
3926
+ ),
3927
+ ]).groups,
3928
+ "alerts",
3929
+ );
3930
+ expect(distinct).toHaveLength(2);
3931
+ expect(distinct.every((group) => group.observations.length === 1)).toBe(
3932
+ true,
3933
+ );
3934
+
3935
+ // Identical definitions from different rule files arrive as identical
3936
+ // rows: two rules this read cannot tell apart, so neither inherits the
3937
+ // other's history on a later read.
3938
+ const twin = () => alertingRule({ alerts: [], state: "inactive" });
3939
+ const twins = groupsOf(
3940
+ project(
3941
+ [
3942
+ diagnoseWithPods("api-68c7b766dc-fmphn"),
3943
+ tool(
3944
+ "rules-1",
3945
+ "get_prometheus_rules",
3946
+ { count: 2, rules: [alertingRule(), twin()] },
3947
+ { summary: rulesArgs },
3948
+ ),
3949
+ ],
3950
+ [
3951
+ tool(
3952
+ "rules-2",
3953
+ "get_prometheus_rules",
3954
+ { count: 2, rules: [alertingRule(), twin()] },
3955
+ { summary: rulesArgs },
3956
+ ),
3957
+ ],
3958
+ ).groups,
3959
+ "alerts",
3960
+ );
3961
+ expect(twins).toHaveLength(4);
3962
+ expect(twins.every((group) => group.observations.length === 1)).toBe(true);
3963
+ expect(twins.map((group) => group.latest.relevance)).toEqual([
3964
+ "target",
3965
+ "broader",
3966
+ "target",
3967
+ "broader",
3968
+ ]);
3969
+ expect(twins.map((group) => group.latest.title)).toEqual([
3970
+ "KubePodCrashLooping firing",
3971
+ "KubePodCrashLooping inactive",
3972
+ "KubePodCrashLooping firing",
3973
+ "KubePodCrashLooping inactive",
3974
+ ]);
3975
+ });
3976
+
3977
+ it("never proves absence by namespace for a cluster-scoped target", () => {
3978
+ const node = { kind: "Node", group: "", name: "worker-1" };
3979
+ const run = (rules: Record<string, unknown>[]) =>
3980
+ groupsOf(
3981
+ projectInvestigationEvidence(
3982
+ [
3983
+ {
3984
+ timeline: [
3985
+ tool(
3986
+ "rules",
3987
+ "get_prometheus_rules",
3988
+ { count: rules.length, rules },
3989
+ { summary: rulesArgs },
3990
+ ),
3991
+ ],
3992
+ },
3993
+ ],
3994
+ node,
3995
+ ).groups,
3996
+ "receipt",
3997
+ );
3998
+ expect(
3999
+ run([
4000
+ alertingRule({
4001
+ name: "KubeNodeNotReady",
4002
+ alerts: [
4003
+ {
4004
+ state: "firing",
4005
+ labels: { node: "worker-1", namespace: "kube-system" },
4006
+ },
4007
+ ],
4008
+ }),
4009
+ ]),
4010
+ ).toHaveLength(0);
4011
+ expect(run([])).toHaveLength(1);
4012
+ expect(run([])[0].latest.title).toBe(
4013
+ "No firing alert rules name this Node",
4014
+ );
4015
+ });
4016
+
4017
+ it("keeps recording rules off the card list and flags truncation and evaluation health", () => {
4018
+ const projection = project([
4019
+ tool(
4020
+ "rules",
4021
+ "get_prometheus_rules",
4022
+ {
4023
+ count: 3,
4024
+ rules: [
4025
+ alertingRule({
4026
+ type: "recording",
4027
+ name: "namespace:container_cpu_usage_seconds_total:sum_rate",
4028
+ alerts: undefined,
4029
+ state: undefined,
4030
+ }),
4031
+ alertingRule({ health: "err", alerts: [] }),
4032
+ alertingRule({
4033
+ name: "KubeContainerWaiting",
4034
+ state: "inactive",
4035
+ health: "unknown",
4036
+ alerts: [],
4037
+ }),
4038
+ ],
4039
+ truncated: true,
4040
+ note: "narrow with name, group, state, or type filters",
4041
+ },
4042
+ { summary: rulesArgs },
4043
+ ),
4044
+ ]);
4045
+ const alerts = groupsOf(projection.groups, "alerts");
4046
+ expect(alerts.map((group) => group.latest.title)).toEqual([
4047
+ "KubePodCrashLooping firing",
4048
+ "KubeContainerWaiting inactive",
4049
+ ]);
4050
+ expect(alerts[1].latest).toMatchObject({
4051
+ tier: "context",
4052
+ tone: "neutral",
4053
+ relevance: "broader",
4054
+ summary: "No active instances · kubernetes-apps",
4055
+ });
4056
+ expect(
4057
+ projection.limitations.map((limitation) => [
4058
+ limitation.source,
4059
+ limitation.kind,
4060
+ ]),
4061
+ ).toEqual([
4062
+ ["Alert rules", "truncated"],
4063
+ ["Alert rule KubePodCrashLooping", "error"],
4064
+ ["Alert rule KubeContainerWaiting", "unknown"],
4065
+ ]);
4066
+ expect(projection.limitations[0].message).toContain(
4067
+ "narrow with name, group, state, or type filters",
4068
+ );
4069
+ expect(groupsOf(projection.groups, "receipt")).toHaveLength(0);
4070
+ });
4071
+
4072
+ it("does not let sampled values churn revision history, and rejects malformed rules", () => {
4073
+ const later = {
4074
+ ...firingInstance,
4075
+ value: "2e+00",
4076
+ activeAt: "2026-09-07T08:10:00Z",
4077
+ };
4078
+ const projection = project(
4079
+ [
4080
+ tool(
4081
+ "rules-1",
4082
+ "get_prometheus_rules",
4083
+ { count: 1, rules: [alertingRule()] },
4084
+ { summary: rulesArgs },
4085
+ ),
4086
+ ],
4087
+ [
4088
+ tool(
4089
+ "rules-2",
4090
+ "get_prometheus_rules",
4091
+ { count: 1, rules: [alertingRule({ alerts: [later] })] },
4092
+ { summary: rulesArgs },
4093
+ ),
4094
+ ],
4095
+ );
4096
+ const group = groupsOf(projection.groups, "alerts")[0];
4097
+ expect(group.observations).toHaveLength(2);
4098
+ expect(group.observations[1].changedFromPrevious).toBe(false);
4099
+
4100
+ const malformed = project([
4101
+ tool(
4102
+ "rules-bad",
4103
+ "get_prometheus_rules",
4104
+ { count: 1, rules: [{ name: "NoGroup", type: "alerting" }] },
4105
+ { summary: rulesArgs },
4106
+ ),
4107
+ tool(
4108
+ "rules-bad-instance",
4109
+ "get_prometheus_rules",
4110
+ { count: 1, rules: [alertingRule({ alerts: [{ labels: {} }] })] },
4111
+ { summary: rulesArgs },
4112
+ ),
4113
+ ]);
4114
+ expect(malformed.groups).toHaveLength(0);
4115
+ expect(malformed.limitations).toHaveLength(1);
4116
+ expect(malformed.limitations[0].sources).toHaveLength(2);
4117
+ expect(malformed.limitations[0].source).toBe("Alert rules");
4118
+
4119
+ const envelope = project([
4120
+ tool(
4121
+ "rules-count",
4122
+ "get_prometheus_rules",
4123
+ { count: 2, rules: [alertingRule()] },
4124
+ { summary: rulesArgs },
4125
+ ),
4126
+ tool(
4127
+ "rules-truncated",
4128
+ "get_prometheus_rules",
4129
+ { count: 0, rules: [], truncated: "true" },
4130
+ { summary: rulesArgs },
4131
+ ),
4132
+ tool(
4133
+ "rules-no-query",
4134
+ "get_prometheus_rules",
4135
+ { count: 1, rules: [alertingRule({ query: undefined })] },
4136
+ { summary: rulesArgs },
4137
+ ),
4138
+ ]);
4139
+ expect(envelope.groups).toHaveLength(0);
4140
+ expect(envelope.limitations[0].sources).toHaveLength(3);
4141
+ });
4142
+ });
4143
+
4144
+ const helmRelease = {
4145
+ name: "shop",
4146
+ namespace: "shop",
4147
+ chart: "shop",
4148
+ chartVersion: "1.4.2",
4149
+ appVersion: "2.0.0",
4150
+ status: "deployed",
4151
+ revision: 7,
4152
+ updated: "2026-09-07T07:00:00Z",
4153
+ description: "Upgrade complete",
4154
+ resources: [
4155
+ {
4156
+ kind: "Deployment",
4157
+ apiVersion: "apps/v1",
4158
+ name: "api",
4159
+ namespace: "shop",
4160
+ status: "Running",
4161
+ ready: "0/1",
4162
+ },
4163
+ {
4164
+ kind: "Service",
4165
+ apiVersion: "v1",
4166
+ name: "api",
4167
+ namespace: "shop",
4168
+ status: "Active",
4169
+ },
4170
+ ],
4171
+ };
4172
+
4173
+ describe("helm release adapter", () => {
4174
+ it("relates a release through the resources it manages", () => {
4175
+ const projection = project([
4176
+ tool("helm", "get_helm_release", helmRelease, {
4177
+ summary: JSON.stringify({ namespace: "shop", name: "shop" }),
4178
+ }),
4179
+ tool(
4180
+ "helm-other",
4181
+ "get_helm_release",
4182
+ {
4183
+ ...helmRelease,
4184
+ name: "ingress",
4185
+ resources: [
4186
+ {
4187
+ kind: "Deployment",
4188
+ apiVersion: "apps/v1",
4189
+ name: "api",
4190
+ namespace: "ingress",
4191
+ },
4192
+ ],
4193
+ },
4194
+ { summary: JSON.stringify({ namespace: "shop", name: "ingress" }) },
4195
+ ),
4196
+ ]);
4197
+ const releases = groupsOf(projection.groups, "helm");
4198
+ expect(releases.map((group) => group.latest.relevance)).toEqual([
4199
+ "producer-related",
4200
+ "broader",
4201
+ ]);
4202
+ expect(releases[0].latest).toMatchObject({
4203
+ tier: "context",
4204
+ tone: "info",
4205
+ title: "Helm release shop/shop",
4206
+ summary: "shop 1.4.2 · deployed · revision 7",
4207
+ });
4208
+ expect(investigationEvidenceSubjectRef(releases[0].latest.data)).toEqual({
4209
+ kind: "HelmRelease",
4210
+ group: "helm.sh",
4211
+ namespace: "shop",
4212
+ name: "shop",
4213
+ });
4214
+ });
4215
+
4216
+ it("never fills a missing owned-resource API version from the target", () => {
4217
+ const projection = project([
4218
+ tool(
4219
+ "helm-no-version",
4220
+ "get_helm_release",
4221
+ {
4222
+ ...helmRelease,
4223
+ resources: [{ kind: "Deployment", name: "api", namespace: "shop" }],
4224
+ },
4225
+ { summary: JSON.stringify({ namespace: "shop", name: "shop" }) },
4226
+ ),
4227
+ tool(
4228
+ "helm-other-group",
4229
+ "get_helm_release",
4230
+ {
4231
+ ...helmRelease,
4232
+ name: "shop-x",
4233
+ resources: [
4234
+ {
4235
+ kind: "Deployment",
4236
+ apiVersion: "example.io/v1",
4237
+ name: "api",
4238
+ namespace: "shop",
4239
+ },
4240
+ ],
4241
+ },
4242
+ { summary: JSON.stringify({ namespace: "shop", name: "shop-x" }) },
4243
+ ),
4244
+ ]);
4245
+ expect(
4246
+ groupsOf(projection.groups, "helm").map(
4247
+ (group) => group.latest.relevance,
4248
+ ),
4249
+ ).toEqual(["broader", "broader"]);
4250
+ });
4251
+
4252
+ it("keys a release by its storage namespace and withholds a link it cannot express", () => {
4253
+ const projection = project([
4254
+ tool(
4255
+ "helm-a",
4256
+ "get_helm_release",
4257
+ { ...helmRelease, storageNamespace: "flux-a" },
4258
+ { summary: JSON.stringify({ namespace: "shop", name: "shop" }) },
4259
+ ),
4260
+ tool(
4261
+ "helm-b",
4262
+ "get_helm_release",
4263
+ { ...helmRelease, storageNamespace: "flux-b", revision: 9 },
4264
+ { summary: JSON.stringify({ namespace: "shop", name: "shop" }) },
4265
+ ),
4266
+ ]);
4267
+ const releases = groupsOf(projection.groups, "helm");
4268
+ expect(releases.map((group) => group.identity)).toEqual([
4269
+ "helm:flux-a:shop:shop",
4270
+ "helm:flux-b:shop:shop",
4271
+ ]);
4272
+ expect(releases[0].latest.data).toMatchObject({
4273
+ release: { storageNamespace: "flux-a" },
4274
+ });
4275
+ expect(investigationEvidenceSubjectRef(releases[0].latest.data)).toBe(
4276
+ undefined,
4277
+ );
4278
+ });
4279
+
4280
+ it("treats a non-deployed status, a health issue, or a failed operation as adverse", () => {
4281
+ const projection = project([
4282
+ tool(
4283
+ "helm-failed",
4284
+ "get_helm_release",
4285
+ { ...helmRelease, status: "failed", description: "Upgrade failed" },
4286
+ { summary: JSON.stringify({ namespace: "shop", name: "shop" }) },
4287
+ ),
4288
+ tool(
4289
+ "helm-issue",
4290
+ "get_helm_release",
4291
+ {
4292
+ ...helmRelease,
4293
+ name: "shop-cache",
4294
+ resourceHealth: "unhealthy",
4295
+ healthIssue: "Deployment shop/cache has 0/1 ready replicas",
4296
+ healthSummary: "1 of 2 resources unhealthy",
4297
+ },
4298
+ { summary: JSON.stringify({ namespace: "shop", name: "shop-cache" }) },
4299
+ ),
4300
+ tool(
4301
+ "helm-stuck",
4302
+ "get_helm_release",
4303
+ {
4304
+ ...helmRelease,
4305
+ name: "shop-jobs",
4306
+ status: "pending-upgrade",
4307
+ lastOperation: {
4308
+ kind: "pending",
4309
+ status: "stuck_pending",
4310
+ source: "helm_status",
4311
+ confidence: "high",
4312
+ message: "Revision 8 has been pending-upgrade for 42m",
4313
+ revision: 8,
4314
+ },
4315
+ valuesError:
4316
+ 'Radar Cloud role "viewer" cannot view Helm release values (requires member or higher)',
4317
+ diffError:
4318
+ 'Radar Cloud role "viewer" cannot view Helm release diffs (requires member or higher)',
4319
+ valuesDiffError: "values diff failed",
4320
+ notesDiffError: "notes diff failed",
4321
+ resourceDiffError: "resource diff failed",
4322
+ },
4323
+ { summary: JSON.stringify({ namespace: "shop", name: "shop-jobs" }) },
4324
+ ),
4325
+ ]);
4326
+ const releases = groupsOf(projection.groups, "helm");
4327
+ expect(
4328
+ releases.map((group) => [group.latest.tier, group.latest.tone]),
4329
+ ).toEqual([
4330
+ ["supporting", "error"],
4331
+ ["supporting", "warning"],
4332
+ ["supporting", "warning"],
4333
+ ]);
4334
+ expect(releases[1].latest.summary).toBe(
4335
+ "deployed · Deployment shop/cache has 0/1 ready replicas",
4336
+ );
4337
+ expect(releases[2].latest.data).toMatchObject({
4338
+ type: "helm",
4339
+ release: {
4340
+ lastOperation: { kind: "pending", status: "stuck_pending" },
4341
+ },
4342
+ });
4343
+ // Every comparison the producer could not finish has to reach the reader:
4344
+ // three of these are role denials, and a release card that omits a withheld
4345
+ // diff without saying so reads as a complete one.
4346
+ expect(projection.limitations).toEqual([
4347
+ expect.objectContaining({ source: "Helm values", kind: "error" }),
4348
+ expect.objectContaining({ source: "Helm values diff", kind: "error" }),
4349
+ expect.objectContaining({ source: "Helm manifest diff", kind: "error" }),
4350
+ expect.objectContaining({ source: "Helm notes diff", kind: "error" }),
4351
+ expect.objectContaining({ source: "Helm resource diff", kind: "error" }),
4352
+ ]);
4353
+ });
4354
+
4355
+ it("rejects malformed release payloads", () => {
4356
+ const projection = project([
4357
+ tool(
4358
+ "helm-bad",
4359
+ "get_helm_release",
4360
+ { ...helmRelease, revision: "7" },
4361
+ { summary: JSON.stringify({ namespace: "shop", name: "shop" }) },
4362
+ ),
4363
+ tool(
4364
+ "helm-bad-resource",
4365
+ "get_helm_release",
4366
+ { ...helmRelease, resources: [{ kind: "Deployment" }] },
4367
+ { summary: JSON.stringify({ namespace: "shop", name: "shop" }) },
4368
+ ),
4369
+ tool(
4370
+ "helm-bad-operation",
4371
+ "get_helm_release",
4372
+ { ...helmRelease, lastOperation: { kind: "rollback" } },
4373
+ { summary: JSON.stringify({ namespace: "shop", name: "shop" }) },
4374
+ ),
4375
+ ]);
4376
+ expect(projection.groups).toHaveLength(0);
4377
+ expect(
4378
+ projection.limitations.map((limitation) => limitation.source),
4379
+ ).toEqual(["Helm release", "Helm resources", "Helm operation"]);
4380
+ });
4381
+ });
4382
+
4383
+ const deploymentWithServiceAccount = {
4384
+ ...deployment,
4385
+ spec: { template: { spec: { serviceAccountName: "api-sa" } } },
4386
+ };
4387
+
4388
+ const permissionsArgs = JSON.stringify({
4389
+ kind: "ServiceAccount",
4390
+ namespace: "shop",
4391
+ name: "api-sa",
4392
+ verb: "get",
4393
+ resource: "secrets",
4394
+ });
4395
+
4396
+ describe("subject permissions adapter", () => {
4397
+ it("does not turn an unevaluated access check into a denial", () => {
4398
+ // A webhook authorizer returning partial data has established neither
4399
+ // answer. Reading it as "cannot" sends an operator after an RBAC grant
4400
+ // that may already exist.
4401
+ const projection = project([
4402
+ tool("diagnose", "diagnose", {
4403
+ resource: deploymentWithServiceAccount,
4404
+ resourceContext: { tier: "basic" },
4405
+ }),
4406
+ tool(
4407
+ "perm",
4408
+ "get_subject_permissions",
4409
+ {
4410
+ subject: { kind: "ServiceAccount", namespace: "shop", name: "api-sa" },
4411
+ accessCheck: {
4412
+ verb: "get",
4413
+ resource: "secrets",
4414
+ namespace: "shop",
4415
+ allowed: false,
4416
+ denied: false,
4417
+ reason: "",
4418
+ evaluationError: "webhook authorizer returned partial data",
4419
+ },
4420
+ },
4421
+ { summary: permissionsArgs },
4422
+ ),
4423
+ ]);
4424
+ const [group] = groupsOf(projection.groups, "permissions");
4425
+ expect(group.latest.title).toContain("Could not check whether");
4426
+ expect(group.latest.title).not.toContain("cannot get");
4427
+ expect(group.latest.summary).toContain("webhook authorizer returned partial data");
4428
+ expect(group.latest.summary).not.toContain("No RBAC rule allows it");
4429
+ // The gap still reaches the coverage strip as well.
4430
+ expect(projection.limitations).toContainEqual(
4431
+ expect.objectContaining({ source: "Permissions", kind: "error" }),
4432
+ );
4433
+ });
4434
+
4435
+ it("keeps a decided verdict that arrived with an evaluation error", () => {
4436
+ // Kubernetes returns the error alongside a real answer too — one webhook
4437
+ // failing while another allows. That answer is still the answer.
4438
+ const projection = project([
4439
+ tool("diagnose", "diagnose", {
4440
+ resource: deploymentWithServiceAccount,
4441
+ resourceContext: { tier: "basic" },
4442
+ }),
4443
+ tool(
4444
+ "perm",
4445
+ "get_subject_permissions",
4446
+ {
4447
+ subject: { kind: "ServiceAccount", namespace: "shop", name: "api-sa" },
4448
+ accessCheck: {
4449
+ verb: "get",
4450
+ resource: "secrets",
4451
+ namespace: "shop",
4452
+ allowed: true,
4453
+ denied: false,
4454
+ reason: "allowed by one authorizer",
4455
+ evaluationError: "a second webhook did not answer",
4456
+ },
4457
+ },
4458
+ { summary: permissionsArgs },
4459
+ ),
4460
+ ]);
4461
+ const [group] = groupsOf(projection.groups, "permissions");
4462
+ expect(group.latest.title).toContain("can get");
4463
+ expect(group.latest.title).not.toContain("Could not check");
4464
+ expect(group.latest.tone).toBe("info");
4465
+ // And the error is still reported, just not as the verdict.
4466
+ expect(projection.limitations).toContainEqual(
4467
+ expect.objectContaining({ source: "Permissions", kind: "error" }),
4468
+ );
4469
+ });
4470
+
4471
+ it("binds an access check to the target's ServiceAccount seen in the same turn", () => {
4472
+ const check = {
4473
+ subject: { kind: "ServiceAccount", namespace: "shop", name: "api-sa" },
4474
+ accessCheck: {
4475
+ verb: "get",
4476
+ resource: "secrets",
4477
+ namespace: "shop",
4478
+ allowed: false,
4479
+ reason: "",
4480
+ },
4481
+ };
4482
+ const projection = project([
4483
+ tool("diagnose", "diagnose", {
4484
+ resource: deploymentWithServiceAccount,
4485
+ resourceContext: { tier: "basic" },
4486
+ }),
4487
+ tool("perm", "get_subject_permissions", check, {
4488
+ summary: permissionsArgs,
4489
+ }),
4490
+ tool(
4491
+ "perm-other",
4492
+ "get_subject_permissions",
4493
+ {
4494
+ subject: { kind: "ServiceAccount", namespace: "shop", name: "other" },
4495
+ accessCheck: {
4496
+ verb: "list",
4497
+ group: "apps",
4498
+ resource: "deployments",
4499
+ subresource: "scale",
4500
+ namespace: "",
4501
+ allowed: true,
4502
+ reason: 'RBAC: allowed by ClusterRoleBinding "admin"',
4503
+ },
4504
+ },
4505
+ {
4506
+ summary: JSON.stringify({
4507
+ kind: "ServiceAccount",
4508
+ namespace: "shop",
4509
+ name: "other",
4510
+ }),
4511
+ },
4512
+ ),
4513
+ ]);
4514
+ const permissions = groupsOf(projection.groups, "permissions");
4515
+ expect(permissions.map((group) => group.latest.relevance)).toEqual([
4516
+ "target",
4517
+ "broader",
4518
+ ]);
4519
+ expect(permissions[0].latest).toMatchObject({
4520
+ tier: "supporting",
4521
+ tone: "warning",
4522
+ title: "Service Account shop/api-sa cannot get secrets",
4523
+ summary: "No RBAC rule allows it · namespace shop",
4524
+ });
4525
+ expect(permissions[0].latest.data).toMatchObject({
4526
+ type: "permissions",
4527
+ accessCheck: { allowed: false, denied: false },
4528
+ });
4529
+ expect(permissions[1].latest).toMatchObject({
4530
+ tier: "context",
4531
+ tone: "info",
4532
+ title: "Service Account shop/other can list deployments/scale.apps",
4533
+ summary: 'RBAC: allowed by ClusterRoleBinding "admin" · cluster-wide',
4534
+ });
4535
+ expect(investigationEvidenceSubjectRef(permissions[0].latest.data)).toEqual(
4536
+ { kind: "ServiceAccount", namespace: "shop", name: "api-sa" },
4537
+ );
4538
+ });
4539
+
4540
+ it("uses the producer's serviceAccount reference and the default account when the template omits one", () => {
4541
+ const withContext = project([
4542
+ tool("diagnose", "diagnose", {
4543
+ resource: deployment,
4544
+ resourceContext: {
4545
+ tier: "diagnostic",
4546
+ uses: {
4547
+ serviceAccount: {
4548
+ kind: "ServiceAccount",
4549
+ namespace: "shop",
4550
+ name: "ctx-sa",
4551
+ },
4552
+ },
4553
+ },
4554
+ }),
4555
+ tool(
4556
+ "perm",
4557
+ "get_subject_permissions",
4558
+ {
4559
+ subject: {
4560
+ kind: "ServiceAccount",
4561
+ namespace: "shop",
4562
+ name: "ctx-sa",
4563
+ },
4564
+ bindings: [],
4565
+ flatRules: [],
4566
+ },
4567
+ { summary: permissionsArgs },
4568
+ ),
4569
+ ]);
4570
+ expect(
4571
+ groupsOf(withContext.groups, "permissions")[0].latest.relevance,
4572
+ ).toBe("target");
4573
+
4574
+ const withDefault = project([
4575
+ tool("resource", "get_resource", {
4576
+ ...deployment,
4577
+ spec: { template: { spec: {} } },
4578
+ }),
4579
+ tool(
4580
+ "perm",
4581
+ "get_subject_permissions",
4582
+ {
4583
+ subject: {
4584
+ kind: "ServiceAccount",
4585
+ namespace: "shop",
4586
+ name: "default",
4587
+ },
4588
+ bindings: [],
4589
+ flatRules: [],
4590
+ },
4591
+ { summary: permissionsArgs },
4592
+ ),
4593
+ ]);
4594
+ expect(
4595
+ groupsOf(withDefault.groups, "permissions")[0].latest.relevance,
4596
+ ).toBe("target");
4597
+
4598
+ const priorTurn = project(
4599
+ [
4600
+ tool("diagnose", "diagnose", {
4601
+ resource: deploymentWithServiceAccount,
4602
+ resourceContext: { tier: "basic" },
4603
+ }),
4604
+ ],
4605
+ [
4606
+ tool(
4607
+ "perm",
4608
+ "get_subject_permissions",
4609
+ {
4610
+ subject: {
4611
+ kind: "ServiceAccount",
4612
+ namespace: "shop",
4613
+ name: "api-sa",
4614
+ },
4615
+ bindings: [],
4616
+ flatRules: [],
4617
+ },
4618
+ { summary: permissionsArgs },
4619
+ ),
4620
+ ],
4621
+ );
4622
+ expect(groupsOf(priorTurn.groups, "permissions")[0].latest.relevance).toBe(
4623
+ "broader",
4624
+ );
4625
+
4626
+ // The newest captured read of the target decides which account it runs as.
4627
+ const rotated = project([
4628
+ tool("resource-old", "get_resource", deploymentWithServiceAccount),
4629
+ tool("resource-new", "get_resource", {
4630
+ ...deployment,
4631
+ spec: { template: { spec: { serviceAccountName: "api-sa-v2" } } },
4632
+ }),
4633
+ tool(
4634
+ "perm-old",
4635
+ "get_subject_permissions",
4636
+ {
4637
+ subject: {
4638
+ kind: "ServiceAccount",
4639
+ namespace: "shop",
4640
+ name: "api-sa",
4641
+ },
4642
+ bindings: [],
4643
+ flatRules: [],
4644
+ },
4645
+ { summary: permissionsArgs },
4646
+ ),
4647
+ tool(
4648
+ "perm-new",
4649
+ "get_subject_permissions",
4650
+ {
4651
+ subject: {
4652
+ kind: "ServiceAccount",
4653
+ namespace: "shop",
4654
+ name: "api-sa-v2",
4655
+ },
4656
+ bindings: [],
4657
+ flatRules: [],
4658
+ },
4659
+ { summary: permissionsArgs },
4660
+ ),
4661
+ ]);
4662
+ expect(
4663
+ groupsOf(rotated.groups, "permissions").map(
4664
+ (group) => group.latest.relevance,
4665
+ ),
4666
+ ).toEqual(["broader", "target"]);
4667
+ });
4668
+
4669
+ it("reads a PodSpec only from kinds that carry one", () => {
4670
+ const check = (name: string) => ({
4671
+ subject: { kind: "ServiceAccount", namespace: "shop", name },
4672
+ accessCheck: {
4673
+ verb: "get",
4674
+ resource: "secrets",
4675
+ namespace: "shop",
4676
+ allowed: false,
4677
+ },
4678
+ });
4679
+ const applicationSet = project([
4680
+ tool("resource", "get_resource", {
4681
+ apiVersion: "argoproj.io/v1alpha1",
4682
+ kind: "ApplicationSet",
4683
+ metadata: { namespace: "shop", name: "api" },
4684
+ spec: { template: { spec: { project: "default" } } },
4685
+ }),
4686
+ tool("perm", "get_subject_permissions", check("default"), {
4687
+ summary: permissionsArgs,
4688
+ }),
4689
+ ]);
4690
+ expect(
4691
+ groupsOf(applicationSet.groups, "permissions")[0].latest.relevance,
4692
+ ).toBe("broader");
4693
+
4694
+ const cronJob = projectInvestigationEvidence(
4695
+ [
4696
+ {
4697
+ timeline: [
4698
+ tool("resource", "get_resource", {
4699
+ apiVersion: "batch/v1",
4700
+ kind: "CronJob",
4701
+ metadata: { namespace: "shop", name: "api" },
4702
+ spec: {
4703
+ jobTemplate: {
4704
+ spec: {
4705
+ template: { spec: { serviceAccountName: "cron-sa" } },
4706
+ },
4707
+ },
4708
+ },
4709
+ }),
4710
+ tool("perm", "get_subject_permissions", check("cron-sa"), {
4711
+ summary: permissionsArgs,
4712
+ }),
4713
+ ],
4714
+ },
4715
+ ],
4716
+ { kind: "CronJob", group: "batch", namespace: "shop", name: "api" },
4717
+ );
4718
+ expect(groupsOf(cronJob.groups, "permissions")[0].latest.relevance).toBe(
4719
+ "target",
4720
+ );
4721
+ });
4722
+
4723
+ it("keeps a denial for an unrelated principal out of the lead evidence", () => {
4724
+ const projection = project([
4725
+ tool("diagnose", "diagnose", {
4726
+ resource: deploymentWithServiceAccount,
4727
+ resourceContext: { tier: "basic" },
4728
+ }),
4729
+ tool(
4730
+ "perm-cross",
4731
+ "get_subject_permissions",
4732
+ {
4733
+ subject: {
4734
+ kind: "ServiceAccount",
4735
+ namespace: "unrelated",
4736
+ name: "other",
4737
+ },
4738
+ accessCheck: {
4739
+ verb: "get",
4740
+ resource: "secrets",
4741
+ namespace: "unrelated",
4742
+ allowed: false,
4743
+ },
4744
+ },
4745
+ {
4746
+ summary: JSON.stringify({
4747
+ kind: "ServiceAccount",
4748
+ namespace: "unrelated",
4749
+ name: "other",
4750
+ }),
4751
+ },
4752
+ ),
4753
+ ]);
4754
+ const denial = groupsOf(projection.groups, "permissions")[0].latest;
4755
+ expect(denial.relevance).toBe("broader");
4756
+ expect(denial.tier).toBe("context");
4757
+ expect(denial.tone).toBe("warning");
4758
+ });
4759
+
4760
+ it("renders the subject response as counts and keeps every coverage caveat", () => {
4761
+ const projection = project([
4762
+ tool(
4763
+ "perm",
4764
+ "get_subject_permissions",
4765
+ {
4766
+ subject: {
4767
+ kind: "ServiceAccount",
4768
+ namespace: "shop",
4769
+ name: "api-sa",
4770
+ },
4771
+ bindings: [
4772
+ {
4773
+ bindingKind: "RoleBinding",
4774
+ bindingNamespace: "shop",
4775
+ bindingName: "api-reader",
4776
+ roleKind: "Role",
4777
+ roleNamespace: "shop",
4778
+ roleName: "reader",
4779
+ rulesCount: 3,
4780
+ },
4781
+ {
4782
+ bindingKind: "ClusterRoleBinding",
4783
+ bindingName: "system:basic-user",
4784
+ roleKind: "ClusterRole",
4785
+ roleName: "system:basic-user",
4786
+ rulesCount: 1,
4787
+ inheritedFromGroup: "system:authenticated",
4788
+ },
4789
+ ],
4790
+ flatRules: [
4791
+ { verbs: ["get"], resources: ["pods"] },
4792
+ { verbs: ["list"], resources: ["pods"] },
4793
+ ],
4794
+ truncated: true,
4795
+ usedByPods: ["shop/api-68c7b766dc-fmphn"],
4796
+ podsTotal: 3,
4797
+ narrowHint:
4798
+ "rule list truncated — the subject has more rules than shown; do not treat this list as the subject's complete permissions",
4799
+ },
4800
+ { summary: permissionsArgs },
4801
+ ),
4802
+ tool(
4803
+ "perm-missing",
4804
+ "get_subject_permissions",
4805
+ {
4806
+ subject: { kind: "ServiceAccount", namespace: "shop", name: "ghost" },
4807
+ bindings: [],
4808
+ flatRules: [],
4809
+ subjectWarning:
4810
+ 'no ServiceAccount "ghost" exists in namespace "shop" — this empty result reflects a subject that was never found, not an account without permissions. Check the name and namespace.',
4811
+ },
4812
+ { summary: permissionsArgs },
4813
+ ),
4814
+ ]);
4815
+ const permissions = groupsOf(projection.groups, "permissions");
4816
+ expect(permissions[0].latest).toMatchObject({
4817
+ tier: "context",
4818
+ tone: "info",
4819
+ relevance: "broader",
4820
+ title: "Permissions of Service Account shop/api-sa",
4821
+ summary: "2 bindings · 2+ effective rules",
4822
+ });
4823
+ expect(permissions[0].latest.data).toMatchObject({
4824
+ type: "permissions",
4825
+ flatRulesCount: 2,
4826
+ truncated: true,
4827
+ usedByPods: ["shop/api-68c7b766dc-fmphn"],
4828
+ podsTotal: 3,
4829
+ });
4830
+ expect(permissions[1].latest.summary).toBe(
4831
+ "0 bindings · 0 effective rules",
4832
+ );
4833
+ expect(
4834
+ projection.limitations.map((limitation) => [
4835
+ limitation.source,
4836
+ limitation.kind,
4837
+ ]),
4838
+ ).toEqual([
4839
+ ["Permissions", "truncated"],
4840
+ ["Permissions", "truncated"],
4841
+ ["Permissions", "unknown"],
4842
+ ]);
4843
+ expect(projection.limitations[0].message).toContain("rule list truncated");
4844
+ expect(projection.limitations[1].message).toBe(
4845
+ "Only 1 of 3 pods running as this subject were listed.",
4846
+ );
4847
+ });
4848
+
4849
+ it("rejects malformed permission payloads", () => {
4850
+ const projection = project([
4851
+ tool(
4852
+ "perm-bad",
4853
+ "get_subject_permissions",
4854
+ { subject: { kind: "ServiceAccount" }, bindings: [], flatRules: [] },
4855
+ { summary: permissionsArgs },
4856
+ ),
4857
+ tool(
4858
+ "perm-bad-check",
4859
+ "get_subject_permissions",
4860
+ {
4861
+ subject: { kind: "ServiceAccount", namespace: "shop", name: "x" },
4862
+ accessCheck: { verb: "get", resource: "secrets", namespace: "shop" },
4863
+ },
4864
+ { summary: permissionsArgs },
4865
+ ),
4866
+ tool(
4867
+ "perm-bad-binding",
4868
+ "get_subject_permissions",
4869
+ {
4870
+ subject: { kind: "ServiceAccount", namespace: "shop", name: "x" },
4871
+ bindings: [{ bindingKind: "RoleBinding" }],
4872
+ flatRules: [],
4873
+ },
4874
+ { summary: permissionsArgs },
4875
+ ),
4876
+ tool(
4877
+ "perm-bad-rule",
4878
+ "get_subject_permissions",
4879
+ {
4880
+ subject: { kind: "ServiceAccount", namespace: "shop", name: "x" },
4881
+ bindings: [],
4882
+ flatRules: [{ resources: ["pods"] }],
4883
+ },
4884
+ { summary: permissionsArgs },
4885
+ ),
4886
+ ]);
4887
+ expect(projection.groups).toHaveLength(0);
4888
+ expect(
4889
+ projection.limitations.map((limitation) => limitation.source),
4890
+ ).toEqual(["Permissions", "Access check"]);
4891
+ expect(projection.limitations[0].sources).toHaveLength(3);
4892
+ });
4893
+ });
4894
+
4895
+ describe("card leads and deep-link subjects", () => {
4896
+ it("leads an events card with the newest warning's reason and message", () => {
4897
+ const projection = project([
4898
+ tool(
4899
+ "events",
4900
+ "get_events",
4901
+ {
4902
+ events: [
4903
+ {
4904
+ reason: "Scheduled",
4905
+ message: "Successfully assigned shop/api-abc to node-1",
4906
+ type: "Normal",
4907
+ count: 1,
4908
+ lastTimestamp: "2026-09-02T11:00:00Z",
4909
+ },
4910
+ warningEvent,
4911
+ {
4912
+ reason: "Unhealthy",
4913
+ message: "Readiness probe failed: connection refused",
4914
+ type: "Warning",
4915
+ count: 12,
4916
+ lastTimestamp: "2026-09-02T10:30:00Z",
4917
+ },
4918
+ ],
4919
+ },
4920
+ {
4921
+ summary: JSON.stringify({
4922
+ kind: "Deployment",
4923
+ namespace: "shop",
4924
+ name: "api",
4925
+ }),
4926
+ },
4927
+ ),
4928
+ ]);
4929
+ expect(groupsOf(projection.groups, "events")[0].latest.summary).toBe(
4930
+ "Unhealthy: Readiness probe failed: connection refused · 3 event groups · Deployment shop/api",
4931
+ );
4932
+ });
4933
+
4934
+ it("carries the producer's change subject so a card can open its Timeline", () => {
4935
+ const change = {
4936
+ kind: "Deployment",
4937
+ apiVersion: "apps/v1",
4938
+ namespace: "shop",
4939
+ name: "api",
4940
+ changeType: "update",
4941
+ timestamp: "2026-09-02T09:00:00Z",
4942
+ };
4943
+ const projection = project([
4944
+ tool(
4945
+ "changes",
4946
+ "get_changes",
4947
+ { changes: [change] },
4948
+ {
4949
+ summary: JSON.stringify({
4950
+ kind: "Deployment",
4951
+ namespace: "shop",
4952
+ name: "api",
4953
+ }),
4954
+ },
4955
+ ),
4956
+ tool(
4957
+ "changes-ns",
4958
+ "get_changes",
4959
+ { changes: [change] },
4960
+ { summary: JSON.stringify({ namespace: "shop" }) },
4961
+ ),
4962
+ tool("diagnose", "diagnose", {
4963
+ resource: deployment,
4964
+ resourceContext: { tier: "basic" },
4965
+ recentChanges: [change],
4966
+ }),
4967
+ tool("resource", "get_resource", {
4968
+ resource: {
4969
+ ...deployment,
4970
+ metadata: { namespace: "shop", name: "web" },
4971
+ },
4972
+ resourceContext: { tier: "basic" },
4973
+ recentChanges: [change],
4974
+ recentChangesSaturated: false,
4975
+ recentChangesCoverageLimited: false,
4976
+ }),
4977
+ ]);
4978
+ const changes = groupsOf(projection.groups, "changes").map(
4979
+ (group) => (group.latest.data as { subject?: unknown }).subject,
4980
+ );
4981
+ expect(changes).toEqual([
4982
+ { kind: "Deployment", namespace: "shop", name: "api" },
4983
+ undefined,
4984
+ { kind: "Deployment", namespace: "shop", name: "api" },
4985
+ { kind: "Deployment", namespace: "shop", name: "web" },
4986
+ ]);
4987
+ });
4988
+ });
4989
+
4990
+ describe("live-run follow-ups", () => {
4991
+ it("reads the producer's nil-slice null as an empty events or changes result", () => {
4992
+ const projection = project([
4993
+ tool(
4994
+ "events-none",
4995
+ "get_events",
4996
+ { events: null },
4997
+ {
4998
+ summary: JSON.stringify({
4999
+ kind: "Pod",
5000
+ namespace: "shop",
5001
+ name: "api",
5002
+ }),
5003
+ },
5004
+ ),
5005
+ tool(
5006
+ "changes-none",
5007
+ "get_changes",
5008
+ { changes: null },
5009
+ {
5010
+ summary: JSON.stringify({
5011
+ kind: "Deployment",
5012
+ namespace: "shop",
5013
+ name: "api",
5014
+ since: "24h",
5015
+ }),
5016
+ },
5017
+ ),
5018
+ ]);
5019
+ // A complete, successful events query that returned nothing is a checked
5020
+ // receipt for its scope; the producer marks a denied namespace separately.
5021
+ expect(
5022
+ projection.groups.map((group) => [
5023
+ group.latest.data.type,
5024
+ group.latest.title,
5025
+ ]),
5026
+ ).toEqual([["receipt", "No events matched"]]);
5027
+ expect(
5028
+ projection.limitations.map((limitation) => [
5029
+ limitation.source,
5030
+ limitation.kind,
5031
+ ]),
5032
+ ).toEqual([["Recent changes", "unknown"]]);
5033
+ });
5034
+
5035
+ it("merges identical startup blockers across pods into one card with the pod list", () => {
5036
+ const blocker = (
5037
+ name: string,
5038
+ message = "1 node(s) no free host ports",
5039
+ ) => ({
5040
+ kind: "Pod",
5041
+ name,
5042
+ reason: "Unschedulable",
5043
+ severity: "critical",
5044
+ message,
5045
+ });
5046
+ const projection = project([
5047
+ tool("diagnose", "diagnose", {
5048
+ resource: {
5049
+ apiVersion: "apps/v1",
5050
+ kind: "DaemonSet",
5051
+ metadata: { namespace: "opencost", name: "node-exporter" },
5052
+ },
5053
+ resourceContext: { tier: "basic" },
5054
+ pods: 3,
5055
+ startupBlockers: [
5056
+ blocker("node-exporter-a"),
5057
+ blocker("node-exporter-b"),
5058
+ blocker("node-exporter-c", "0/10 nodes: insufficient memory"),
5059
+ ],
5060
+ }),
5061
+ ]);
5062
+ const startup = projection.groups.filter(
5063
+ (group) => group.latest.data.type === "startup",
5064
+ );
5065
+ expect(
5066
+ startup.map((group) => [
5067
+ group.latest.summary,
5068
+ group.latest.data.type === "startup" ? group.latest.data.pods : null,
5069
+ ]),
5070
+ ).toEqual([
5071
+ [
5072
+ "2 pods · 1 node(s) no free host ports",
5073
+ ["node-exporter-a", "node-exporter-b"],
5074
+ ],
5075
+ ["0/10 nodes: insufficient memory", undefined],
5076
+ ]);
5077
+ // A single pod keeps the per-pod identity so saved runs still match.
5078
+ expect(startup[1].id).not.toBe(startup[0].id);
5079
+ });
5080
+
5081
+ it("folds pods' startup blocker into the classified issue that repeats it", () => {
5082
+ const blocker = (name: string) => ({
5083
+ kind: "Pod",
5084
+ name,
5085
+ reason: "Unschedulable",
5086
+ severity: "critical",
5087
+ message: "1 node(s) no free host ports",
5088
+ });
5089
+ const projection = project([
5090
+ tool("diagnose", "diagnose", {
5091
+ resource: {
5092
+ apiVersion: "apps/v1",
5093
+ kind: "DaemonSet",
5094
+ metadata: { namespace: "opencost", name: "node-exporter" },
5095
+ },
5096
+ resourceContext: { tier: "basic" },
5097
+ pods: 2,
5098
+ relatedIssues: [
5099
+ {
5100
+ id: "issue-unsched",
5101
+ severity: "critical",
5102
+ source: "scheduling",
5103
+ category: "unschedulable",
5104
+ category_group: "startup",
5105
+ grouping_scope: "workload",
5106
+ kind: "DaemonSet",
5107
+ namespace: "opencost",
5108
+ name: "node-exporter",
5109
+ reason: "Unschedulable",
5110
+ message: "1 node(s) no free host ports",
5111
+ },
5112
+ ],
5113
+ startupBlockers: [
5114
+ blocker("node-exporter-a"),
5115
+ blocker("node-exporter-b"),
5116
+ ],
5117
+ }),
5118
+ ]);
5119
+ expect(
5120
+ projection.groups.filter((group) => group.latest.data.type === "startup"),
5121
+ ).toHaveLength(0);
5122
+ const [issue] = projection.groups.filter(
5123
+ (group) => group.latest.data.type === "issue",
5124
+ );
5125
+ expect(
5126
+ issue.latest.data.type === "issue" && issue.latest.data.pods,
5127
+ ).toEqual(["node-exporter-a", "node-exporter-b"]);
5128
+ });
5129
+
5130
+ it("files a denied events namespace as an access limitation, not a receipt", () => {
5131
+ const projection = project([
5132
+ tool(
5133
+ "events-denied",
5134
+ "get_events",
5135
+ { events: [], accessDenied: true },
5136
+ {
5137
+ summary: JSON.stringify({ namespace: "locked", kind: "Pod" }),
5138
+ },
5139
+ ),
5140
+ ]);
5141
+ expect(projection.groups).toHaveLength(0);
5142
+ expect(
5143
+ projection.limitations.map((limitation) => [
5144
+ limitation.source,
5145
+ limitation.kind,
5146
+ ]),
5147
+ ).toEqual([["Events", "error"]]);
5148
+ expect(projection.limitations[0].message).toContain(
5149
+ "not readable with your permissions",
5150
+ );
5151
+ });
5152
+
5153
+ it("keeps repeated change reads of one resource on one card and names the window", () => {
5154
+ const change = {
5155
+ kind: "Deployment",
5156
+ apiVersion: "apps/v1",
5157
+ namespace: "shop",
5158
+ name: "api",
5159
+ changeType: "update",
5160
+ timestamp: "2026-09-02T09:00:00Z",
5161
+ };
5162
+ const older = { ...change, timestamp: "2026-09-01T09:00:00Z" };
5163
+ const read = (
5164
+ id: string,
5165
+ args: Record<string, unknown>,
5166
+ changes: unknown[],
5167
+ ) =>
5168
+ tool(id, "get_changes", { changes }, { summary: JSON.stringify(args) });
5169
+ const projection = project([
5170
+ read(
5171
+ "changes-24h",
5172
+ { namespace: "shop", name: "api", since: "24h", kind: "Deployment" },
5173
+ [change],
5174
+ ),
5175
+ read(
5176
+ "changes-48h",
5177
+ {
5178
+ kind: "Deployment",
5179
+ namespace: "shop",
5180
+ name: "api",
5181
+ since: "48h",
5182
+ limit: 30,
5183
+ },
5184
+ [change, older],
5185
+ ),
5186
+ read("changes-ns", { namespace: "shop", since: "24h" }, [change]),
5187
+ ]);
5188
+ const changes = groupsOf(projection.groups, "changes");
5189
+ expect(changes.map((group) => group.identity)).toEqual([
5190
+ "changes:Deployment shop/api",
5191
+ "changes:namespace shop",
5192
+ ]);
5193
+ expect(changes[0].observations).toHaveLength(2);
5194
+ expect(changes[0].observations.map((o) => o.title)).toEqual([
5195
+ "Recent changes · last 24h",
5196
+ "Recent changes · last 48h",
5197
+ ]);
5198
+ expect(changes[0].latest.title).toBe("Recent changes · last 48h");
5199
+ expect(changes[0].latest.summary).toBe("2 changes · Deployment shop/api");
5200
+ expect(changes[1].latest.title).toBe("Recent changes · last 24h");
5201
+ });
5202
+ });