@skyhook-io/radar-app 1.14.0 → 1.14.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/package.json +2 -2
  2. package/src/App.tsx +6 -4
  3. package/src/api/client.ts +3 -0
  4. package/src/api/diagnose.ts +31 -1
  5. package/src/components/CloudConnectFlow.tsx +173 -13
  6. package/src/components/CloudFunnelButton.tsx +4 -2
  7. package/src/components/ConnectionErrorView.tsx +9 -10
  8. package/src/components/DebugOverlay.tsx +10 -6
  9. package/src/components/cloudConnectHandoff.test.ts +109 -4
  10. package/src/components/cloudConnectHandoff.ts +154 -2
  11. package/src/components/curl/ServiceCurlButton.tsx +19 -26
  12. package/src/components/diagnose/AgentCase.tsx +18 -5
  13. package/src/components/diagnose/AnalysisStory.test.tsx +308 -0
  14. package/src/components/diagnose/AnalysisStory.tsx +564 -0
  15. package/src/components/diagnose/DiagnoseContext.test.ts +10 -0
  16. package/src/components/diagnose/DiagnoseContext.tsx +25 -8
  17. package/src/components/diagnose/DiagnoseSurface.tsx +20 -14
  18. package/src/components/diagnose/InvestigationEvidencePane.story.test.tsx +771 -0
  19. package/src/components/diagnose/InvestigationEvidencePane.test.tsx +8 -33
  20. package/src/components/diagnose/InvestigationEvidencePane.tsx +809 -168
  21. package/src/components/diagnose/InvestigationResourceEvidence.test.tsx +30 -0
  22. package/src/components/diagnose/InvestigationResourceEvidence.tsx +20 -0
  23. package/src/components/diagnose/InvestigationView.tsx +493 -529
  24. package/src/components/diagnose/investigationCase.test.tsx +267 -129
  25. package/src/components/diagnose/investigationCase.ts +122 -77
  26. package/src/components/diagnose/investigationEvidence/adapters/resource.test.ts +27 -0
  27. package/src/components/diagnose/investigationEvidence/adapters/resource.ts +28 -0
  28. package/src/components/diagnose/investigationEvidence/builder.ts +11 -2
  29. package/src/components/diagnose/investigationEvidence/identity.test.ts +25 -7
  30. package/src/components/diagnose/investigationEvidence/identity.ts +4 -4
  31. package/src/components/diagnose/investigationEvidence/observations.ts +24 -0
  32. package/src/components/diagnose/investigationEvidence/projection.test.ts +36 -3
  33. package/src/components/diagnose/investigationEvidence/types.ts +2 -0
  34. package/src/components/diagnose/investigationEvidencePresentation.test.ts +2 -0
  35. package/src/components/diagnose/investigationEvidencePresentation.ts +3 -0
  36. package/src/components/diagnose/investigationState.test.ts +316 -58
  37. package/src/components/diagnose/investigationState.ts +257 -44
  38. package/src/components/diagnose/investigationStory.test.ts +161 -0
  39. package/src/components/diagnose/investigationStory.ts +207 -0
  40. package/src/components/diagnose/parts.test.tsx +9 -6
  41. package/src/components/diagnose/parts.tsx +977 -193
  42. package/src/components/diagnose/storyParts.test.tsx +426 -0
  43. package/src/components/diagnose/toolCallLabel.test.ts +51 -0
  44. package/src/components/diagnose/toolCallLabel.ts +135 -0
  45. package/src/components/diagnose/useDisclosureReveal.ts +10 -11
  46. package/src/components/helm/HelmCompareRoute.tsx +18 -4
  47. package/src/components/helm/HelmReleaseDrawer.tsx +11 -26
  48. package/src/components/helm/InstallWizard.tsx +8 -3
  49. package/src/components/home/MCPSetupDialog.tsx +15 -9
  50. package/src/components/portforward/PortForwardManager.tsx +6 -13
  51. package/src/components/resource/PrometheusChartsGrid.tsx +3 -3
  52. package/src/components/resource/WorkloadMetricsHelpDialog.tsx +3 -3
  53. package/src/components/resource/WorkloadMetricsSection.tsx +15 -21
  54. package/src/components/resources/PodFilePreview.tsx +3 -3
  55. package/src/components/resources/renderers/ServiceRenderer.tsx +2 -1
  56. package/src/components/settings/SettingsDialog.tsx +4 -2
  57. package/src/components/ui/CommandPalette.tsx +10 -6
  58. package/src/components/ui/DiagnosticsOverlay.tsx +10 -6
  59. package/src/components/ui/Disclosure.tsx +47 -0
  60. package/src/components/ui/Markdown.tsx +23 -11
  61. package/src/components/ui/ShortcutHelpOverlay.tsx +3 -1
  62. package/src/components/ui/UpdateNotification.tsx +18 -2
  63. package/src/index.css +19 -6
@@ -18,9 +18,6 @@ import {
18
18
  import {
19
19
  resolveInvestigationCase,
20
20
  type InvestigationCaseResolution,
21
- investigationCaseItemsStillRendered,
22
- mergeInvestigationCases,
23
- type InvestigationCaseItem,
24
21
  } from "./investigationCase";
25
22
  import { AssessmentSources, ResultCard, assessmentSourceRows } from "./parts";
26
23
  import type { Diagnosis, DiagnosisEvidenceItem } from "../../api/diagnose";
@@ -145,6 +142,7 @@ const diagnoseBundle = {
145
142
  },
146
143
  },
147
144
  pods: 1,
145
+ podNames: ["api-abc"],
148
146
  relatedIssues: [criticalIssue],
149
147
  logsCurrent: [
150
148
  { pod: "api-abc", container: "api", logs: logs(["ERROR auth failed"]) },
@@ -188,6 +186,263 @@ describe("agent case placement (D-1, D-1b)", () => {
188
186
  expect(html).not.toContain("data-agent-claim");
189
187
  });
190
188
 
189
+ it("forgives a group on a core kind and a named entry on a listing", () => {
190
+ const listRef = evidenceRef("c", "d");
191
+ const withListing = project(
192
+ tool("diag", "diagnose", diagnoseBundle, { evidenceRef: ref }),
193
+ tool(
194
+ "cms",
195
+ "list_resources",
196
+ [{ kind: "ConfigMap", name: "kube-root-ca.crt" }],
197
+ {
198
+ evidenceRef: listRef,
199
+ summary: JSON.stringify({ kind: "configmaps", namespace: "shop" }),
200
+ },
201
+ ),
202
+ );
203
+ const resolved = resolveInvestigationCase(
204
+ withListing,
205
+ {
206
+ evidence: [
207
+ linked(ref, "cause", "The previous log names the error.", {
208
+ group: "apps",
209
+ kind: "Pod",
210
+ namespace: "shop",
211
+ name: "api-abc",
212
+ container: "api",
213
+ stream: "previous",
214
+ observation: "logs",
215
+ }),
216
+ linked(listRef, "context", "Only the CA bundle exists.", {
217
+ kind: "ConfigMap",
218
+ namespace: "shop",
219
+ name: "kube-root-ca.crt",
220
+ observation: "resource",
221
+ }),
222
+ linked(listRef, "rules_out", "No nginx config lives here.", {
223
+ kind: "ConfigMap",
224
+ namespace: "shop",
225
+ observation: "resource",
226
+ }),
227
+ linked(ref, "symptom", "BackOff keeps firing on the pod.", {
228
+ kind: "Pod",
229
+ namespace: "shop",
230
+ name: "api-abc",
231
+ observation: "events",
232
+ }),
233
+ ],
234
+ },
235
+ 0,
236
+ );
237
+ expect(resolved.items.map((item) => item.placement)).toEqual([
238
+ "card",
239
+ "card",
240
+ "card",
241
+ "card",
242
+ ]);
243
+ });
244
+
245
+ it("binds a Pod subject to events only for the target's own pod or a pod an event names whole", () => {
246
+ const workerRef = evidenceRef("e", "f");
247
+ const canaryBundle = {
248
+ ...diagnoseBundle,
249
+ events: [
250
+ {
251
+ reason: "BackOff",
252
+ message: "Back-off restarting failed container in pod api-abc-canary",
253
+ type: "Warning",
254
+ count: 4,
255
+ lastTimestamp: "2026-09-02T10:00:00Z",
256
+ },
257
+ ],
258
+ };
259
+ const withWorker = project(
260
+ tool("diag", "diagnose", canaryBundle, { evidenceRef: ref }),
261
+ tool(
262
+ "ev",
263
+ "get_events",
264
+ { events: [] },
265
+ {
266
+ evidenceRef: workerRef,
267
+ summary: JSON.stringify({
268
+ kind: "Deployment",
269
+ namespace: "shop",
270
+ name: "worker",
271
+ }),
272
+ },
273
+ ),
274
+ );
275
+ const podEvents = (name: string, cite = ref) =>
276
+ linked(cite, "symptom", `${name} keeps backing off.`, {
277
+ kind: "Pod",
278
+ namespace: "shop",
279
+ name,
280
+ observation: "events",
281
+ });
282
+ const resolved = resolveInvestigationCase(
283
+ withWorker,
284
+ {
285
+ evidence: [
286
+ podEvents("api-abc"),
287
+ podEvents("api-abc-canary"),
288
+ podEvents("abc-canary"),
289
+ podEvents("api-zzz"),
290
+ podEvents("api-abc", workerRef),
291
+ ],
292
+ },
293
+ 0,
294
+ );
295
+ expect(resolved.items.map((item) => item.placement)).toEqual([
296
+ "card",
297
+ "card",
298
+ "source",
299
+ "source",
300
+ "source",
301
+ ]);
302
+ });
303
+
304
+ it("binds an alert citation to the rule it names when one result holds several rules", () => {
305
+ const rulesRef = evidenceRef("i", "j");
306
+ const rule = (name: string) => ({
307
+ group: "kubernetes-apps",
308
+ type: "alerting",
309
+ name,
310
+ query: "vector(1)",
311
+ state: "firing",
312
+ health: "ok",
313
+ labels: { severity: "warning" },
314
+ annotations: { summary: name },
315
+ alerts: [
316
+ {
317
+ state: "firing",
318
+ activeAt: "2026-09-07T07:58:00Z",
319
+ value: "1e+00",
320
+ labels: {
321
+ alertname: name,
322
+ namespace: "shop",
323
+ pod: "api-abc",
324
+ container: "api",
325
+ },
326
+ },
327
+ ],
328
+ });
329
+ const withRules = project(
330
+ tool("diag", "diagnose", diagnoseBundle, { evidenceRef: ref }),
331
+ tool(
332
+ "rules",
333
+ "get_prometheus_rules",
334
+ {
335
+ count: 2,
336
+ rules: [
337
+ rule("KubePodCrashLooping"),
338
+ rule("KubeDeploymentReplicasMismatch"),
339
+ ],
340
+ },
341
+ { evidenceRef: rulesRef, summary: JSON.stringify({ state: "firing" }) },
342
+ ),
343
+ );
344
+ const cite = (name?: string) =>
345
+ linked(rulesRef, "symptom", "The rule is firing for this pod.", {
346
+ kind: "PrometheusRule",
347
+ namespace: "shop",
348
+ ...(name ? { name } : {}),
349
+ observation: "alerts",
350
+ });
351
+ const resolved = resolveInvestigationCase(
352
+ withRules,
353
+ {
354
+ evidence: [
355
+ cite("KubeDeploymentReplicasMismatch"),
356
+ cite("KubePodCrashLooping"),
357
+ cite(),
358
+ ],
359
+ },
360
+ 0,
361
+ );
362
+ expect(resolved.items.map((item) => item.placement)).toEqual([
363
+ "card",
364
+ "card",
365
+ "source",
366
+ ]);
367
+ expect(resolved.items[0].observation?.title).toContain(
368
+ "KubeDeploymentReplicasMismatch",
369
+ );
370
+ const wrongCase = resolveInvestigationCase(
371
+ withRules,
372
+ { evidence: [cite("kubepodcrashlooping")] },
373
+ 0,
374
+ );
375
+ expect(wrongCase.items.map((item) => item.placement)).toEqual(["source"]);
376
+ });
377
+
378
+ it("still binds a named entry the listing does not hold to the listing", () => {
379
+ const listRef = evidenceRef("c", "d");
380
+ const withListing = project(
381
+ tool("diag", "diagnose", diagnoseBundle, { evidenceRef: ref }),
382
+ tool(
383
+ "cms",
384
+ "list_resources",
385
+ [{ kind: "ConfigMap", name: "kube-root-ca.crt" }],
386
+ {
387
+ evidenceRef: listRef,
388
+ summary: JSON.stringify({ kind: "configmaps", namespace: "shop" }),
389
+ },
390
+ ),
391
+ );
392
+ const resolved = resolveInvestigationCase(
393
+ withListing,
394
+ {
395
+ evidence: [
396
+ linked(listRef, "context", "The nginx config is mounted from here.", {
397
+ kind: "ConfigMap",
398
+ namespace: "shop",
399
+ name: "nginx-config",
400
+ observation: "resource",
401
+ }),
402
+ ],
403
+ },
404
+ 0,
405
+ );
406
+ expect(resolved.items.map((item) => item.placement)).toEqual(["card"]);
407
+ });
408
+
409
+ it("places a Namespace citation on a namespace listing, whose call names no kind", () => {
410
+ const nsRef = evidenceRef("g", "h");
411
+ const withNamespaces = project(
412
+ tool("diag", "diagnose", diagnoseBundle, { evidenceRef: ref }),
413
+ tool(
414
+ "ns",
415
+ "list_namespaces",
416
+ [
417
+ { name: "shop", status: "Active" },
418
+ { name: "capi-system", status: "Terminating" },
419
+ ],
420
+ { evidenceRef: nsRef, summary: "{}" },
421
+ ),
422
+ );
423
+ const resolved = resolveInvestigationCase(
424
+ withNamespaces,
425
+ {
426
+ evidence: [
427
+ linked(nsRef, "context", "Only two namespaces exist.", {
428
+ kind: "Namespace",
429
+ observation: "resource",
430
+ }),
431
+ linked(nsRef, "rules_out", "shop is not terminating.", {
432
+ kind: "Namespace",
433
+ name: "shop",
434
+ observation: "resource",
435
+ }),
436
+ ],
437
+ },
438
+ 0,
439
+ );
440
+ expect(resolved.items.map((item) => item.placement)).toEqual([
441
+ "card",
442
+ "card",
443
+ ]);
444
+ });
445
+
191
446
  it("pins a subject-bearing item to exactly the observation it names", () => {
192
447
  const resolved = resolveInvestigationCase(
193
448
  projection,
@@ -210,6 +465,11 @@ describe("agent case placement (D-1, D-1b)", () => {
210
465
  name: "api",
211
466
  observation: "events",
212
467
  }),
468
+ linked(ref, "context", "Namespace-named events land too.", {
469
+ kind: "Namespace",
470
+ name: "shop",
471
+ observation: "events",
472
+ }),
213
473
  linked(
214
474
  ref,
215
475
  "context",
@@ -240,6 +500,7 @@ describe("agent case placement (D-1, D-1b)", () => {
240
500
  kind: "resource",
241
501
  },
242
502
  "Events only repeat the back-off.": { placement: "card", kind: "events" },
503
+ "Namespace-named events land too.": { placement: "card", kind: "events" },
243
504
  "Ambiguous: two observations share this subject.": {
244
505
  placement: "source",
245
506
  kind: undefined,
@@ -1069,130 +1330,6 @@ describe("follow-up answers that cite evidence", () => {
1069
1330
  });
1070
1331
  });
1071
1332
 
1072
- describe("carrying earlier assessments' card notes", () => {
1073
- const item = (
1074
- index: number,
1075
- groupId: string | undefined,
1076
- placement: "card" | "revision" | "source",
1077
- role: "cause" | "context" | "demoted" = "context",
1078
- ) =>
1079
- ({
1080
- index,
1081
- role,
1082
- claim: `claim ${index}`,
1083
- source: { id: `s${index}` },
1084
- placement,
1085
- groupId,
1086
- }) as unknown as InvestigationCaseItem;
1087
-
1088
- it("keeps an earlier turn's note on a card the live turn did not address", () => {
1089
- const live = {
1090
- items: [item(0, "chart", "card")],
1091
- ruledOut: [],
1092
- } as unknown as InvestigationCaseResolution;
1093
- const first = {
1094
- items: [
1095
- item(0, "logs", "card", "demoted"),
1096
- item(1, "chart", "card", "cause"),
1097
- item(2, undefined, "source"),
1098
- item(3, "deploy", "revision"),
1099
- ],
1100
- ruledOut: [{ hypothesis: "x" }],
1101
- } as unknown as InvestigationCaseResolution;
1102
- const merged = mergeInvestigationCases(live, [first]);
1103
- expect(merged?.items.map((entry) => [entry.groupId, entry.role])).toEqual([
1104
- ["chart", "context"],
1105
- ["logs", "demoted"],
1106
- ["deploy", "context"],
1107
- ]);
1108
- expect(merged?.ruledOut).toEqual([]);
1109
- });
1110
-
1111
- it("lets the newer earlier turn win a card and leaves a lone live case untouched", () => {
1112
- const live = {
1113
- items: [],
1114
- ruledOut: [],
1115
- } as unknown as InvestigationCaseResolution;
1116
- const newer = {
1117
- items: [item(0, "logs", "card", "context")],
1118
- ruledOut: [],
1119
- } as unknown as InvestigationCaseResolution;
1120
- const older = {
1121
- items: [item(0, "logs", "card", "demoted"), item(1, "pod", "card")],
1122
- ruledOut: [],
1123
- } as unknown as InvestigationCaseResolution;
1124
- const merged = mergeInvestigationCases(live, [newer, older]);
1125
- expect(merged?.items.map((entry) => [entry.groupId, entry.role])).toEqual([
1126
- ["logs", "context"],
1127
- ["pod", "context"],
1128
- ]);
1129
- expect(mergeInvestigationCases(live, [])).toBe(live);
1130
- expect(mergeInvestigationCases(undefined, [])).toBeUndefined();
1131
- });
1132
-
1133
- it("recognises its own surviving notes across a re-resolved merge", () => {
1134
- // The pane feeds the merge freshly resolved copies of every turn, so the
1135
- // assessment's own items arrive equal but not identical. An identity test
1136
- // reported them as gone, which silently retired the conflict banner's
1137
- // reframing whenever any unrelated follow-up arrived.
1138
- const assessmentItems = [
1139
- item(0, "logs", "card", "demoted"),
1140
- item(1, "crash", "card", "demoted"),
1141
- ];
1142
- const reResolved = assessmentItems.map(
1143
- (entry) => ({ ...entry }) as InvestigationCaseItem,
1144
- );
1145
- expect(reResolved[0]).not.toBe(assessmentItems[0]);
1146
- expect(
1147
- investigationCaseItemsStillRendered(assessmentItems, reResolved).map(
1148
- (entry) => entry.groupId,
1149
- ),
1150
- ).toEqual(["logs", "crash"]);
1151
- // A note the merge dropped is correctly reported as gone.
1152
- expect(
1153
- investigationCaseItemsStillRendered(assessmentItems, [reResolved[0]]).map(
1154
- (entry) => entry.groupId,
1155
- ),
1156
- ).toEqual(["logs"]);
1157
- expect(investigationCaseItemsStillRendered(assessmentItems, [])).toEqual(
1158
- [],
1159
- );
1160
- });
1161
-
1162
- it("carries every note the winning assessment left on a card, not just the first", () => {
1163
- // A card note and a note pinned to a superseded read are two readings of
1164
- // one group by the same assessment. Taking coverage per item dropped the
1165
- // second, so a visible Cause note could vanish behind a revision note.
1166
- const live = {
1167
- items: [item(0, "other", "card", "context")],
1168
- ruledOut: [],
1169
- } as unknown as InvestigationCaseResolution;
1170
- const winner = {
1171
- items: [
1172
- item(1, "logs", "revision", "context"),
1173
- item(2, "logs", "card", "cause"),
1174
- ],
1175
- ruledOut: [],
1176
- } as unknown as InvestigationCaseResolution;
1177
- const older = {
1178
- items: [item(3, "logs", "card", "demoted")],
1179
- ruledOut: [],
1180
- } as unknown as InvestigationCaseResolution;
1181
- const merged = mergeInvestigationCases(live, [winner, older]);
1182
- expect(
1183
- merged?.items.map((entry) => [
1184
- entry.groupId,
1185
- entry.placement,
1186
- entry.role,
1187
- ]),
1188
- ).toEqual([
1189
- ["other", "card", "context"],
1190
- ["logs", "revision", "context"],
1191
- ["logs", "card", "cause"],
1192
- ]);
1193
- });
1194
- });
1195
-
1196
1333
  describe("agent case robustness", () => {
1197
1334
  const ref = evidenceRef("a", "b");
1198
1335
 
@@ -1294,7 +1431,7 @@ describe("agent case robustness", () => {
1294
1431
  );
1295
1432
  });
1296
1433
 
1297
- it("keeps valid items when siblings are unlinked, malformed, foreign, or from another turn", () => {
1434
+ it("keeps valid items when siblings are unlinked or malformed, and places an earlier turn's read", () => {
1298
1435
  const stale = evidenceRef("a", "z");
1299
1436
  const projection = projectInvestigationEvidence(
1300
1437
  [
@@ -1346,6 +1483,7 @@ describe("agent case robustness", () => {
1346
1483
  );
1347
1484
  expect(resolved.items.map((item) => [item.index, item.placement])).toEqual([
1348
1485
  [1, "card"],
1486
+ [4, "revision"],
1349
1487
  [5, "source"],
1350
1488
  ]);
1351
1489
  expect(resolved.ruledOut.map((entry) => entry.hypothesis)).toEqual([
@@ -1553,7 +1691,7 @@ describe("the agent's contribution is one attributed row", () => {
1553
1691
 
1554
1692
  it("labels the role that says an adverse result is not a live problem", () => {
1555
1693
  const html = renderToStaticMarkup(<AgentRoleChip role="benign" />);
1556
- expect(html).toContain("Not a problem");
1694
+ expect(html).toContain("Not a live problem");
1557
1695
  });
1558
1696
 
1559
1697
  it("still shows an attributed role when the agent left no sentence", () => {