@skyhook-io/radar-app 1.13.5 → 1.14.1

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 (242) hide show
  1. package/package.json +9 -9
  2. package/src/App.tsx +66 -28
  3. package/src/api/client.authRedirect.test.ts +12 -1
  4. package/src/api/client.prometheusStatus.test.ts +24 -0
  5. package/src/api/client.ts +324 -16
  6. package/src/api/diagnose.ts +32 -1
  7. package/src/api/drain-plan.test.ts +86 -0
  8. package/src/api/quotas.test.ts +26 -0
  9. package/src/api/quotas.ts +20 -0
  10. package/src/api/workloadMetrics.test.ts +40 -0
  11. package/src/api/workloadMetrics.ts +68 -0
  12. package/src/components/CloudConnectFlow.tsx +343 -46
  13. package/src/components/CloudFunnelButton.tsx +110 -37
  14. package/src/components/ConnectionErrorView.tsx +9 -10
  15. package/src/components/DebugOverlay.tsx +10 -6
  16. package/src/components/SyncProgressPanel.tsx +73 -0
  17. package/src/components/audit/AuditView.tsx +2 -0
  18. package/src/components/cloudConnectHandoff.test.ts +245 -0
  19. package/src/components/cloudConnectHandoff.ts +276 -0
  20. package/src/components/curl/ServiceCurlButton.tsx +19 -26
  21. package/src/components/diagnose/AgentCase.tsx +18 -5
  22. package/src/components/diagnose/AnalysisStory.test.tsx +308 -0
  23. package/src/components/diagnose/AnalysisStory.tsx +564 -0
  24. package/src/components/diagnose/DiagnoseContext.test.ts +10 -0
  25. package/src/components/diagnose/DiagnoseContext.tsx +26 -8
  26. package/src/components/diagnose/DiagnoseSurface.tsx +20 -14
  27. package/src/components/diagnose/InvestigationEvidencePane.story.test.tsx +771 -0
  28. package/src/components/diagnose/InvestigationEvidencePane.test.tsx +8 -33
  29. package/src/components/diagnose/InvestigationEvidencePane.tsx +809 -168
  30. package/src/components/diagnose/InvestigationResourceEvidence.test.tsx +30 -0
  31. package/src/components/diagnose/InvestigationResourceEvidence.tsx +20 -0
  32. package/src/components/diagnose/InvestigationView.tsx +493 -529
  33. package/src/components/diagnose/investigationCase.test.tsx +267 -129
  34. package/src/components/diagnose/investigationCase.ts +122 -77
  35. package/src/components/diagnose/investigationEvidence/adapters/changes.ts +60 -0
  36. package/src/components/diagnose/investigationEvidence/adapters/diagnose.test.ts +573 -0
  37. package/src/components/diagnose/investigationEvidence/adapters/diagnose.ts +935 -0
  38. package/src/components/diagnose/investigationEvidence/adapters/events.ts +96 -0
  39. package/src/components/diagnose/investigationEvidence/adapters/helm.test.ts +242 -0
  40. package/src/components/diagnose/investigationEvidence/adapters/helm.ts +208 -0
  41. package/src/components/diagnose/investigationEvidence/adapters/issues.ts +172 -0
  42. package/src/components/diagnose/investigationEvidence/adapters/logs.test.ts +283 -0
  43. package/src/components/diagnose/investigationEvidence/adapters/logs.ts +183 -0
  44. package/src/components/diagnose/investigationEvidence/adapters/permissions.test.ts +528 -0
  45. package/src/components/diagnose/investigationEvidence/adapters/permissions.ts +292 -0
  46. package/src/components/diagnose/investigationEvidence/adapters/prometheus.test.ts +1641 -0
  47. package/src/components/diagnose/investigationEvidence/adapters/prometheus.ts +769 -0
  48. package/src/components/diagnose/investigationEvidence/adapters/resource.test.ts +81 -0
  49. package/src/components/diagnose/investigationEvidence/adapters/resource.ts +285 -0
  50. package/src/components/diagnose/investigationEvidence/adapters/topology.ts +207 -0
  51. package/src/components/diagnose/investigationEvidence/builder.ts +427 -0
  52. package/src/components/diagnose/investigationEvidence/evidenceFixtures.ts +179 -0
  53. package/src/components/diagnose/investigationEvidence/identity.test.ts +632 -0
  54. package/src/components/diagnose/investigationEvidence/identity.ts +261 -0
  55. package/src/components/diagnose/investigationEvidence/index.ts +55 -0
  56. package/src/components/diagnose/investigationEvidence/observations.ts +978 -0
  57. package/src/components/diagnose/investigationEvidence/parse.ts +467 -0
  58. package/src/components/diagnose/investigationEvidence/projection.test.ts +2736 -0
  59. package/src/components/diagnose/investigationEvidence/types.ts +514 -0
  60. package/src/components/diagnose/investigationEvidencePresentation.test.ts +2 -0
  61. package/src/components/diagnose/investigationEvidencePresentation.ts +3 -0
  62. package/src/components/diagnose/investigationState.test.ts +316 -58
  63. package/src/components/diagnose/investigationState.ts +257 -44
  64. package/src/components/diagnose/investigationStory.test.ts +161 -0
  65. package/src/components/diagnose/investigationStory.ts +207 -0
  66. package/src/components/diagnose/parts.test.tsx +9 -6
  67. package/src/components/diagnose/parts.tsx +977 -193
  68. package/src/components/diagnose/storyParts.test.tsx +426 -0
  69. package/src/components/diagnose/target.test.ts +19 -0
  70. package/src/components/diagnose/target.ts +3 -2
  71. package/src/components/diagnose/toolCallLabel.test.ts +51 -0
  72. package/src/components/diagnose/toolCallLabel.ts +135 -0
  73. package/src/components/diagnose/useDisclosureReveal.ts +10 -11
  74. package/src/components/gitops/GitOpsView.tsx +7 -0
  75. package/src/components/gitops/RemoteDestinationCloudHint.tsx +26 -0
  76. package/src/components/helm/HelmCompareRoute.tsx +18 -4
  77. package/src/components/helm/HelmReleaseDrawer.tsx +11 -26
  78. package/src/components/helm/InstallWizard.tsx +8 -3
  79. package/src/components/home/MCPSetupDialog.tsx +15 -9
  80. package/src/components/portforward/PortForwardManager.tsx +6 -13
  81. package/src/components/resource/PrometheusCharts.overlays.test.ts +22 -0
  82. package/src/components/resource/PrometheusCharts.tsx +4 -3
  83. package/src/components/resource/PrometheusChartsGrid.render.test.tsx +99 -0
  84. package/src/components/resource/PrometheusChartsGrid.tsx +75 -35
  85. package/src/components/resource/RightsizingStrip.test.ts +38 -0
  86. package/src/components/resource/RightsizingStrip.tsx +15 -3
  87. package/src/components/resource/WorkloadMetricsHelpDialog.test.tsx +79 -0
  88. package/src/components/resource/WorkloadMetricsHelpDialog.tsx +145 -0
  89. package/src/components/resource/WorkloadMetricsSection.render.test.tsx +351 -0
  90. package/src/components/resource/WorkloadMetricsSection.tsx +480 -0
  91. package/src/components/resource/workloadMetricValues.test.ts +65 -0
  92. package/src/components/resource/workloadMetricValues.ts +30 -0
  93. package/src/components/resources/PodFilePreview.tsx +3 -3
  94. package/src/components/resources/ResourcesView.tsx +44 -6
  95. package/src/components/resources/renderers/NamespaceRenderer.tsx +7 -1
  96. package/src/components/resources/renderers/PodRenderer.tsx +4 -0
  97. package/src/components/resources/renderers/RolloutRenderer.tsx +15 -1
  98. package/src/components/resources/renderers/ServiceRenderer.tsx +2 -1
  99. package/src/components/resources/renderers/WorkloadRenderer.tsx +3 -0
  100. package/src/components/rightsizing/RightsizingScanView.tsx +21 -0
  101. package/src/components/settings/SettingsDialog.tsx +98 -16
  102. package/src/components/traffic/TrafficFlowList.tsx +229 -70
  103. package/src/components/traffic/TrafficGraph.tsx +6 -6
  104. package/src/components/traffic/TrafficView.tsx +10 -9
  105. package/src/components/traffic/trafficFilters.test.ts +28 -1
  106. package/src/components/traffic/trafficFilters.ts +31 -0
  107. package/src/components/ui/CommandPalette.tsx +10 -6
  108. package/src/components/ui/DiagnosticsOverlay.tsx +10 -6
  109. package/src/components/ui/Disclosure.tsx +47 -0
  110. package/src/components/ui/Markdown.tsx +23 -11
  111. package/src/components/ui/ShortcutHelpOverlay.tsx +3 -1
  112. package/src/components/ui/UpdateNotification.tsx +18 -2
  113. package/src/components/workload/WorkloadView.tsx +19 -8
  114. package/src/context/ConnectionContext.tsx +48 -3
  115. package/src/context/DiagnoseCustomization.tsx +1 -0
  116. package/src/index.css +36 -6
  117. package/src/utils/navigation.ts +1 -1
  118. package/src/utils/topology-namespace.test.ts +64 -0
  119. package/src/utils/topology-namespace.ts +18 -0
  120. package/src/components/diagnose/investigationEvidence.test.ts +0 -6617
  121. package/src/components/diagnose/investigationEvidence.ts +0 -5572
  122. package/src/components/gitops/GitOpsActions.tsx +0 -1
  123. package/src/components/gitops/GitOpsStatusBadge.tsx +0 -1
  124. package/src/components/gitops/ManagedResourcesList.tsx +0 -1
  125. package/src/components/gitops/SyncCountdown.tsx +0 -1
  126. package/src/components/gitops/index.ts +0 -4
  127. package/src/components/helm/ManifestDiffViewer.tsx +0 -69
  128. package/src/components/logs/JsonLogLine.tsx +0 -1
  129. package/src/components/logs/LogCore.tsx +0 -2
  130. package/src/components/logs/useLogBuffer.ts +0 -2
  131. package/src/components/logs/useLogSearch.ts +0 -1
  132. package/src/components/resource-drawer/ResourceDrawer.tsx +0 -216
  133. package/src/components/resources/drawer-components.tsx +0 -1
  134. package/src/components/resources/renderers/AlertRenderer.tsx +0 -1
  135. package/src/components/resources/renderers/CNPGBackupRenderer.tsx +0 -1
  136. package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +0 -1
  137. package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +0 -1
  138. package/src/components/resources/renderers/CertificateRenderer.tsx +0 -1
  139. package/src/components/resources/renderers/CertificateRequestRenderer.tsx +0 -1
  140. package/src/components/resources/renderers/ChallengeRenderer.tsx +0 -1
  141. package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +0 -1
  142. package/src/components/resources/renderers/ClusterExternalSecretRenderer.tsx +0 -1
  143. package/src/components/resources/renderers/ClusterIssuerRenderer.tsx +0 -1
  144. package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +0 -1
  145. package/src/components/resources/renderers/ConfigMapRenderer.tsx +0 -1
  146. package/src/components/resources/renderers/CronJobRenderer.tsx +0 -1
  147. package/src/components/resources/renderers/CronWorkflowRenderer.tsx +0 -1
  148. package/src/components/resources/renderers/EventRenderer.tsx +0 -1
  149. package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +0 -1
  150. package/src/components/resources/renderers/ExternalSecretRenderer.tsx +0 -1
  151. package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +0 -1
  152. package/src/components/resources/renderers/GRPCRouteRenderer.tsx +0 -1
  153. package/src/components/resources/renderers/GatewayClassRenderer.tsx +0 -1
  154. package/src/components/resources/renderers/GatewayRenderer.tsx +0 -1
  155. package/src/components/resources/renderers/GenericRenderer.tsx +0 -1
  156. package/src/components/resources/renderers/GitRepositoryRenderer.tsx +0 -1
  157. package/src/components/resources/renderers/HTTPRouteRenderer.tsx +0 -1
  158. package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +0 -1
  159. package/src/components/resources/renderers/IngressClassRenderer.tsx +0 -1
  160. package/src/components/resources/renderers/IngressRenderer.tsx +0 -1
  161. package/src/components/resources/renderers/IstioAuthorizationPolicyRenderer.tsx +0 -1
  162. package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +0 -1
  163. package/src/components/resources/renderers/IstioGatewayRenderer.tsx +0 -1
  164. package/src/components/resources/renderers/IstioPeerAuthenticationRenderer.tsx +0 -1
  165. package/src/components/resources/renderers/IstioServiceEntryRenderer.tsx +0 -1
  166. package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +0 -1
  167. package/src/components/resources/renderers/JobRenderer.tsx +0 -1
  168. package/src/components/resources/renderers/KarpenterEC2NodeClassRenderer.tsx +0 -1
  169. package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +0 -1
  170. package/src/components/resources/renderers/KedaScaledJobRenderer.tsx +0 -1
  171. package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +0 -1
  172. package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +0 -1
  173. package/src/components/resources/renderers/KnativeConfigurationRenderer.tsx +0 -1
  174. package/src/components/resources/renderers/KnativeEventingRenderer.tsx +0 -1
  175. package/src/components/resources/renderers/KnativeFlowRenderer.tsx +0 -1
  176. package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +0 -1
  177. package/src/components/resources/renderers/KnativeRevisionRenderer.tsx +0 -1
  178. package/src/components/resources/renderers/KnativeRouteRenderer.tsx +0 -1
  179. package/src/components/resources/renderers/KnativeServiceRenderer.tsx +0 -1
  180. package/src/components/resources/renderers/KnativeSourceRenderer.tsx +0 -1
  181. package/src/components/resources/renderers/KustomizationRenderer.tsx +0 -1
  182. package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +0 -1
  183. package/src/components/resources/renderers/LeaseRenderer.tsx +0 -1
  184. package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +0 -1
  185. package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +0 -1
  186. package/src/components/resources/renderers/OrderRenderer.tsx +0 -1
  187. package/src/components/resources/renderers/PersistentVolumeRenderer.tsx +0 -1
  188. package/src/components/resources/renderers/PodDisruptionBudgetRenderer.tsx +0 -1
  189. package/src/components/resources/renderers/PodMonitorRenderer.tsx +0 -1
  190. package/src/components/resources/renderers/PriorityClassRenderer.tsx +0 -1
  191. package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +0 -1
  192. package/src/components/resources/renderers/ReplicaSetRenderer.tsx +0 -1
  193. package/src/components/resources/renderers/RuntimeClassRenderer.tsx +0 -1
  194. package/src/components/resources/renderers/SbomReportRenderer.tsx +0 -1
  195. package/src/components/resources/renderers/SealedSecretRenderer.tsx +0 -1
  196. package/src/components/resources/renderers/SecretRenderer.tsx +0 -1
  197. package/src/components/resources/renderers/SecretStoreRenderer.tsx +0 -1
  198. package/src/components/resources/renderers/ServiceMonitorRenderer.tsx +0 -1
  199. package/src/components/resources/renderers/SimpleRouteRenderer.tsx +0 -1
  200. package/src/components/resources/renderers/StorageClassRenderer.tsx +0 -1
  201. package/src/components/resources/renderers/TraefikIngressRouteRenderer.tsx +0 -1
  202. package/src/components/resources/renderers/VPARenderer.tsx +0 -1
  203. package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +0 -1
  204. package/src/components/resources/renderers/VeleroVSLRenderer.tsx +0 -1
  205. package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +0 -1
  206. package/src/components/resources/renderers/WebhookConfigRenderer.tsx +0 -1
  207. package/src/components/resources/renderers/WorkflowRenderer.tsx +0 -1
  208. package/src/components/resources/renderers/WorkflowTemplateRenderer.tsx +0 -1
  209. package/src/components/resources/renderers/argo-cells.tsx +0 -1
  210. package/src/components/resources/renderers/certmanager-cells.tsx +0 -1
  211. package/src/components/resources/renderers/cnpg-cells.tsx +0 -1
  212. package/src/components/resources/renderers/eso-cells.tsx +0 -1
  213. package/src/components/resources/renderers/flux-cells.tsx +0 -1
  214. package/src/components/resources/renderers/index.ts +0 -94
  215. package/src/components/resources/renderers/istio-cells.tsx +0 -1
  216. package/src/components/resources/renderers/karpenter-cells.tsx +0 -1
  217. package/src/components/resources/renderers/keda-cells.tsx +0 -1
  218. package/src/components/resources/renderers/knative-cells.tsx +0 -1
  219. package/src/components/resources/renderers/kyverno-cells.tsx +0 -1
  220. package/src/components/resources/renderers/prometheus-cells.tsx +0 -1
  221. package/src/components/resources/renderers/traefik-cells.tsx +0 -1
  222. package/src/components/resources/renderers/trivy-cells.tsx +0 -1
  223. package/src/components/resources/renderers/trivy-shared.tsx +0 -1
  224. package/src/components/resources/renderers/velero-cells.tsx +0 -1
  225. package/src/components/resources/resource-utils-argo.ts +0 -2
  226. package/src/components/resources/resource-utils-certmanager.ts +0 -2
  227. package/src/components/resources/resource-utils-eso.ts +0 -2
  228. package/src/components/resources/resource-utils-flux.ts +0 -2
  229. package/src/components/resources/resource-utils-istio.ts +0 -2
  230. package/src/components/resources/resource-utils-karpenter.ts +0 -2
  231. package/src/components/resources/resource-utils-knative.ts +0 -2
  232. package/src/components/resources/resource-utils-kyverno.ts +0 -2
  233. package/src/components/resources/resource-utils-prometheus.ts +0 -2
  234. package/src/components/resources/resource-utils-traefik.ts +0 -1
  235. package/src/components/resources/resource-utils-trivy.ts +0 -2
  236. package/src/components/resources/resource-utils-velero.ts +0 -2
  237. package/src/components/shared/EditableYamlView.tsx +0 -24
  238. package/src/components/timeline/DiffViewer.tsx +0 -1
  239. package/src/components/traffic/index.ts +0 -3
  240. package/src/components/ui/ForceDeleteConfirmDialog.tsx +0 -1
  241. package/src/components/ui/MetricsChart.tsx +0 -1
  242. package/src/components/ui/ResourceBar.tsx +0 -1
@@ -0,0 +1,771 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { renderToStaticMarkup } from "react-dom/server";
3
+
4
+ import {
5
+ InvestigationEvidencePane,
6
+ namedInventoryRows,
7
+ } from "./InvestigationEvidencePane";
8
+ import { resolveInvestigationCase } from "./investigationCase";
9
+ import {
10
+ projectInvestigationEvidence,
11
+ type InvestigationEvidenceTimelineItem,
12
+ } from "./investigationEvidence";
13
+ import type { DiagnosisEvidenceItem } from "../../api/diagnose";
14
+
15
+ const target = {
16
+ kind: "Deployment",
17
+ group: "apps",
18
+ namespace: "shop",
19
+ name: "api",
20
+ };
21
+
22
+ function evidenceRef(scope: string, nonce: string): string {
23
+ return `ev_${scope.repeat(26)}_${nonce.repeat(26)}`;
24
+ }
25
+
26
+ function tool(
27
+ id: string,
28
+ name: string,
29
+ result: unknown,
30
+ patch: Partial<
31
+ Extract<InvestigationEvidenceTimelineItem, { kind: "tool" }>
32
+ > = {},
33
+ ): Extract<InvestigationEvidenceTimelineItem, { kind: "tool" }> {
34
+ return {
35
+ kind: "tool",
36
+ id,
37
+ tool: name,
38
+ status: "done",
39
+ summary: JSON.stringify({ namespace: "shop", name: "api" }),
40
+ result: JSON.stringify(result),
41
+ isError: false,
42
+ radarEvidence: true,
43
+ ...patch,
44
+ };
45
+ }
46
+
47
+ const crashIssue = {
48
+ id: "issue-api-crash",
49
+ severity: "critical",
50
+ source: "problem",
51
+ category: "crashloop",
52
+ category_group: "runtime",
53
+ grouping_scope: "workload",
54
+ kind: "Deployment",
55
+ group: "apps",
56
+ namespace: "shop",
57
+ name: "api",
58
+ reason: "CrashLoopBackOff",
59
+ message: "The API container keeps restarting.",
60
+ };
61
+
62
+ describe("InvestigationEvidencePane under a story", () => {
63
+ const ref = evidenceRef("a", "b");
64
+ const projection = projectInvestigationEvidence(
65
+ [
66
+ {
67
+ timeline: [
68
+ tool(
69
+ "issues",
70
+ "issues",
71
+ { issues: [crashIssue], total: 1, total_matched: 1 },
72
+ {
73
+ evidenceRef: ref,
74
+ },
75
+ ),
76
+ ],
77
+ },
78
+ ],
79
+ target,
80
+ );
81
+ const evidence: DiagnosisEvidenceItem[] = [
82
+ { status: "linked", ref, role: "cause", claim: "" },
83
+ { status: "unlinked" },
84
+ ];
85
+ const investigationCase = resolveInvestigationCase(
86
+ projection,
87
+ { evidence },
88
+ 0,
89
+ );
90
+ const render = (report: string) =>
91
+ renderToStaticMarkup(
92
+ <InvestigationEvidencePane
93
+ projection={projection}
94
+ investigationCase={investigationCase}
95
+ story={{ report, evidence }}
96
+ collecting={false}
97
+ animateGroupIds={new Set()}
98
+ onViewSource={() => {}}
99
+ onViewActivity={() => {}}
100
+ afterEvidence={<div data-next-steps>steps</div>}
101
+ />,
102
+ );
103
+
104
+ it("places the cited card in the story, marks it in the inventory, and puts next steps before it", () => {
105
+ const html = render(
106
+ "The container keeps dying.\n\n[[radar:evidence=0]]\n\nThat is the whole story.",
107
+ );
108
+ expect(html).toContain("data-story");
109
+ expect(html).toContain('id="story-');
110
+ expect(html).toContain("Captured results");
111
+ expect(html).toContain("1 shown above");
112
+ expect(html).toContain("In the analysis");
113
+ expect(html).not.toContain(">Evidence<");
114
+ // Next steps sit between the story and the inventory.
115
+ const story = html.indexOf("data-story");
116
+ const steps = html.indexOf("data-next-steps");
117
+ const inventory = html.indexOf("Captured results");
118
+ expect(story).toBeGreaterThan(-1);
119
+ expect(steps).toBeGreaterThan(story);
120
+ expect(inventory).toBeGreaterThan(steps);
121
+ // The inventory is collapsed once the story placed something.
122
+ expect(html).toMatch(/aria-controls="investigation-captured-results"[^>]*/);
123
+ expect(html).toMatch(
124
+ /aria-expanded="false"[^>]*aria-controls="investigation-captured-results"/,
125
+ );
126
+ });
127
+
128
+ it("shows lost support at the sentence and opens the inventory when nothing was placed", () => {
129
+ const html = render(
130
+ "Nothing placed, but see [[radar:evidence=1]] and [[radar:evidence=7]].",
131
+ );
132
+ expect(html).toContain('data-story-lost-support="unlinked"');
133
+ expect(html).toContain('data-story-lost-support="invalid"');
134
+ });
135
+
136
+ it("links a bound call that produced no card to its raw result in Activity", () => {
137
+ const searchRef = evidenceRef("e", "f");
138
+ const withSearch = projectInvestigationEvidence(
139
+ [
140
+ {
141
+ timeline: [
142
+ tool(
143
+ "issues",
144
+ "issues",
145
+ { issues: [crashIssue], total: 1, total_matched: 1 },
146
+ {
147
+ evidenceRef: ref,
148
+ },
149
+ ),
150
+ tool(
151
+ "search",
152
+ "search",
153
+ { results: [], total: 0 },
154
+ {
155
+ evidenceRef: searchRef,
156
+ summary: JSON.stringify({ query: "metrics-server" }),
157
+ },
158
+ ),
159
+ ],
160
+ },
161
+ ],
162
+ target,
163
+ );
164
+ const items: DiagnosisEvidenceItem[] = [
165
+ {
166
+ status: "linked",
167
+ ref: searchRef,
168
+ role: "rules_out",
169
+ claim: "Nothing named metrics-server exists.",
170
+ },
171
+ ];
172
+ const html = renderToStaticMarkup(
173
+ <InvestigationEvidencePane
174
+ projection={withSearch}
175
+ investigationCase={resolveInvestigationCase(
176
+ withSearch,
177
+ { evidence: items },
178
+ 0,
179
+ )}
180
+ story={{
181
+ report: "No metrics-server anywhere.\n\n[[radar:evidence=0]]",
182
+ evidence: items,
183
+ }}
184
+ collecting={false}
185
+ animateGroupIds={new Set()}
186
+ onViewSource={() => {}}
187
+ onViewActivity={() => {}}
188
+ />,
189
+ );
190
+ expect(html).toContain('data-story-lost-support="nocard"');
191
+ expect(html).toContain("Result in Activity");
192
+ expect(html).toContain("None shown above");
193
+ expect(html).toMatch(
194
+ /aria-expanded="true"[^>]*aria-controls="investigation-captured-results"/,
195
+ );
196
+ });
197
+
198
+ it("places a subject-less citation of a fan-out call on that call's primary card", () => {
199
+ const bundleRef = evidenceRef("c", "d");
200
+ const bundle = projectInvestigationEvidence(
201
+ [
202
+ {
203
+ timeline: [
204
+ tool(
205
+ "diag",
206
+ "diagnose",
207
+ {
208
+ resource: {
209
+ apiVersion: "apps/v1",
210
+ kind: "Deployment",
211
+ metadata: { name: "api", namespace: "shop" },
212
+ status: { replicas: 1, readyReplicas: 0 },
213
+ },
214
+ issues: [crashIssue],
215
+ events: [],
216
+ },
217
+ { evidenceRef: bundleRef },
218
+ ),
219
+ ],
220
+ },
221
+ ],
222
+ target,
223
+ );
224
+ const items: DiagnosisEvidenceItem[] = [
225
+ { status: "linked", ref: bundleRef, role: "context", claim: "" },
226
+ ];
227
+ const bundleCase = resolveInvestigationCase(bundle, { evidence: items }, 0);
228
+ // The resolver itself leaves the item at its source: several cards match.
229
+ expect(bundleCase.items[0]?.placement).toBe("source");
230
+ const html = renderToStaticMarkup(
231
+ <InvestigationEvidencePane
232
+ projection={bundle}
233
+ investigationCase={bundleCase}
234
+ story={{
235
+ report: "Radar's bundle says so.\n\n[[radar:evidence=0]]",
236
+ evidence: items,
237
+ }}
238
+ collecting={false}
239
+ animateGroupIds={new Set()}
240
+ onViewSource={() => {}}
241
+ onViewActivity={() => {}}
242
+ />,
243
+ );
244
+ expect(html).toContain('data-story-placement="0"');
245
+ expect(html).not.toContain("data-story-lost-support");
246
+ expect(html).toContain("1 shown above");
247
+ });
248
+
249
+ it("renders a cited earlier read as that read, labelled, when a newer one exists", () => {
250
+ const early = evidenceRef("a", "b");
251
+ const later = evidenceRef("c", "d");
252
+ const twoReads = projectInvestigationEvidence(
253
+ [
254
+ {
255
+ timeline: [
256
+ tool(
257
+ "issues-early",
258
+ "issues",
259
+ { issues: [crashIssue], total: 1, total_matched: 1 },
260
+ {
261
+ evidenceRef: early,
262
+ },
263
+ ),
264
+ ],
265
+ },
266
+ {
267
+ timeline: [
268
+ tool(
269
+ "issues-later",
270
+ "issues",
271
+ {
272
+ issues: [
273
+ {
274
+ ...crashIssue,
275
+ message:
276
+ "The API container keeps restarting, 40 times now.",
277
+ },
278
+ ],
279
+ total: 1,
280
+ total_matched: 1,
281
+ },
282
+ { evidenceRef: later },
283
+ ),
284
+ ],
285
+ },
286
+ ],
287
+ target,
288
+ );
289
+ const items: DiagnosisEvidenceItem[] = [
290
+ { status: "linked", ref: early, role: "cause", claim: "" },
291
+ ];
292
+ // A revised assessment in turn 1 citing the read from turn 0.
293
+ const html = renderToStaticMarkup(
294
+ <InvestigationEvidencePane
295
+ projection={twoReads}
296
+ investigationCase={resolveInvestigationCase(
297
+ twoReads,
298
+ { evidence: items },
299
+ 1,
300
+ )}
301
+ story={{
302
+ report: "It was crashing then.\n\n[[radar:evidence=0]]",
303
+ evidence: items,
304
+ }}
305
+ collecting={false}
306
+ animateGroupIds={new Set()}
307
+ onViewSource={() => {}}
308
+ onViewActivity={() => {}}
309
+ />,
310
+ );
311
+ expect(html).toContain('data-story-placement="0"');
312
+ expect(html).toContain("Captured in turn 1");
313
+ expect(html).toContain("a newer read");
314
+ });
315
+
316
+ it("binds a subject-less earlier citation to that call's own read, never a later one", () => {
317
+ const early = evidenceRef("a", "b");
318
+ const later = evidenceRef("c", "d");
319
+ const bundleResult = (ready: number) => ({
320
+ resource: {
321
+ apiVersion: "apps/v1",
322
+ kind: "Deployment",
323
+ metadata: { name: "api", namespace: "shop" },
324
+ status: { replicas: 1, readyReplicas: ready },
325
+ },
326
+ issues: [crashIssue],
327
+ events: [],
328
+ });
329
+ const twoBundles = projectInvestigationEvidence(
330
+ [
331
+ {
332
+ timeline: [
333
+ tool("diag-early", "diagnose", bundleResult(0), {
334
+ evidenceRef: early,
335
+ }),
336
+ ],
337
+ },
338
+ {
339
+ timeline: [
340
+ tool("diag-later", "diagnose", bundleResult(1), {
341
+ evidenceRef: later,
342
+ }),
343
+ ],
344
+ },
345
+ ],
346
+ target,
347
+ );
348
+ const items: DiagnosisEvidenceItem[] = [
349
+ { status: "linked", ref: early, role: "cause", claim: "" },
350
+ ];
351
+ const html = renderToStaticMarkup(
352
+ <InvestigationEvidencePane
353
+ projection={twoBundles}
354
+ investigationCase={resolveInvestigationCase(
355
+ twoBundles,
356
+ { evidence: items },
357
+ 1,
358
+ )}
359
+ story={{
360
+ report: "Then it was down.\n\n[[radar:evidence=0]]",
361
+ evidence: items,
362
+ }}
363
+ collecting={false}
364
+ animateGroupIds={new Set()}
365
+ onViewSource={() => {}}
366
+ onViewActivity={() => {}}
367
+ />,
368
+ );
369
+ const placed = html.slice(html.indexOf('data-story-placement="0"'));
370
+ expect(placed).toContain('data-evidence-source="turn-0-step-diag-early"');
371
+ expect(placed).toContain("Captured in turn 1");
372
+ });
373
+
374
+ it("keeps the previous layout without a story", () => {
375
+ const html = renderToStaticMarkup(
376
+ <InvestigationEvidencePane
377
+ projection={projection}
378
+ investigationCase={investigationCase}
379
+ collecting={false}
380
+ animateGroupIds={new Set()}
381
+ onViewSource={() => {}}
382
+ onViewActivity={() => {}}
383
+ afterEvidence={<div data-next-steps>steps</div>}
384
+ />,
385
+ );
386
+ expect(html).toContain(">Evidence<");
387
+ expect(html).not.toContain("Captured results");
388
+ expect(html.indexOf("data-next-steps")).toBeGreaterThan(
389
+ html.indexOf("More evidence about this resource"),
390
+ );
391
+ });
392
+ });
393
+
394
+ describe("story log card", () => {
395
+ const ref = evidenceRef("c", "d");
396
+ const projection = projectInvestigationEvidence(
397
+ [
398
+ {
399
+ timeline: [
400
+ tool(
401
+ "wl",
402
+ "get_workload_logs",
403
+ {
404
+ workload: "deployments/shop/api",
405
+ pods: 1,
406
+ logs: [
407
+ {
408
+ pod: "api-68c7b766dc-fmphn",
409
+ container: "api",
410
+ logs: {
411
+ lines: [
412
+ "2026-09-07T08:00:19Z boot",
413
+ "2026-09-07T08:00:20Z connecting",
414
+ "2026-09-07T08:00:21Z MongoServerError: Authentication failed.",
415
+ ],
416
+ totalLines: 50,
417
+ matchedLines: 0,
418
+ fallback: true,
419
+ },
420
+ },
421
+ ],
422
+ },
423
+ {
424
+ evidenceRef: ref,
425
+ summary: JSON.stringify({
426
+ namespace: "shop",
427
+ name: "api",
428
+ kind: "deployment",
429
+ }),
430
+ },
431
+ ),
432
+ ],
433
+ },
434
+ ],
435
+ target,
436
+ );
437
+ const evidence: DiagnosisEvidenceItem[] = [
438
+ {
439
+ status: "linked",
440
+ ref,
441
+ role: "cause",
442
+ claim: "The log names the failure.",
443
+ },
444
+ ];
445
+ const investigationCase = resolveInvestigationCase(
446
+ projection,
447
+ { evidence },
448
+ 0,
449
+ );
450
+ const html = renderToStaticMarkup(
451
+ <InvestigationEvidencePane
452
+ projection={projection}
453
+ investigationCase={investigationCase}
454
+ story={{
455
+ report: "The API cannot log in.\n\n[[radar:evidence=0]]\n",
456
+ evidence,
457
+ }}
458
+ collecting={false}
459
+ animateGroupIds={new Set()}
460
+ onViewSource={() => {}}
461
+ onViewActivity={() => {}}
462
+ />,
463
+ );
464
+ const story = html.slice(
465
+ html.indexOf('aria-label="Analysis"'),
466
+ html.indexOf("investigation-captured-results"),
467
+ );
468
+
469
+ it("shows the cited lines inline and keeps retrieval bookkeeping out of the story", () => {
470
+ expect(story).toContain("MongoServerError: Authentication failed.");
471
+ expect(story).toContain("Earlier lines");
472
+ expect(story).not.toContain("matched the filter");
473
+ expect(story).not.toContain("filter matched nothing");
474
+ expect(html).toContain("0 of 50 read lines matched the filter");
475
+ });
476
+ });
477
+
478
+ describe("story card gap clause", () => {
479
+ it("puts the clause on a compact card's row and under a full card without a label", () => {
480
+ const ref = evidenceRef("q", "r");
481
+ const projection = projectInvestigationEvidence(
482
+ [
483
+ {
484
+ timeline: [
485
+ tool(
486
+ "issues",
487
+ "issues",
488
+ { issues: [crashIssue], total: 1, total_matched: 1 },
489
+ {
490
+ evidenceRef: ref,
491
+ },
492
+ ),
493
+ ],
494
+ },
495
+ ],
496
+ target,
497
+ );
498
+ const evidence: DiagnosisEvidenceItem[] = [
499
+ {
500
+ status: "linked",
501
+ ref,
502
+ role: "cause",
503
+ claim: "",
504
+ gap: "this pod only, not the other replicas",
505
+ },
506
+ ];
507
+ const render = (report: string) =>
508
+ renderToStaticMarkup(
509
+ <InvestigationEvidencePane
510
+ projection={projection}
511
+ investigationCase={resolveInvestigationCase(
512
+ projection,
513
+ { evidence },
514
+ 0,
515
+ )}
516
+ story={{ report, evidence }}
517
+ collecting={false}
518
+ animateGroupIds={new Set()}
519
+ onViewSource={() => {}}
520
+ onViewActivity={() => {}}
521
+ />,
522
+ );
523
+ const compact = render("It crashes.\n\n[[radar:evidence=0|compact]]");
524
+ expect(compact).toContain("data-agent-gap-hint");
525
+ expect(compact).toContain("this pod only, not the other replicas");
526
+ expect(compact).not.toContain("not shown");
527
+ const full = render("It crashes.\n\n[[radar:evidence=0]]");
528
+ // The record under Captured results keeps its labelled form.
529
+ const story = full.slice(0, full.indexOf("Captured results"));
530
+ expect(story).toMatch(
531
+ /data-agent-gap[^>]*>this pod only, not the other replicas</,
532
+ );
533
+ expect(story).not.toContain("Not shown:");
534
+ });
535
+ });
536
+
537
+ describe("story card defaults", () => {
538
+ it("shows the row a citation names on a listing card, inline", () => {
539
+ const listRef = evidenceRef("k", "l");
540
+ const projection = projectInvestigationEvidence(
541
+ [
542
+ {
543
+ timeline: [
544
+ tool(
545
+ "cms",
546
+ "list_resources",
547
+ [
548
+ { kind: "ConfigMap", name: "kube-root-ca.crt" },
549
+ { kind: "ConfigMap", name: "nginx-config" },
550
+ ],
551
+ {
552
+ evidenceRef: listRef,
553
+ summary: JSON.stringify({
554
+ kind: "configmaps",
555
+ namespace: "shop",
556
+ }),
557
+ },
558
+ ),
559
+ ],
560
+ },
561
+ ],
562
+ target,
563
+ );
564
+ const evidence: DiagnosisEvidenceItem[] = [
565
+ {
566
+ status: "linked",
567
+ ref: listRef,
568
+ role: "context",
569
+ claim: "",
570
+ subject: {
571
+ kind: "ConfigMap",
572
+ namespace: "shop",
573
+ name: "nginx-config",
574
+ observation: "resource",
575
+ },
576
+ },
577
+ ];
578
+ const html = renderToStaticMarkup(
579
+ <InvestigationEvidencePane
580
+ projection={projection}
581
+ investigationCase={resolveInvestigationCase(
582
+ projection,
583
+ { evidence },
584
+ 0,
585
+ )}
586
+ story={{
587
+ report: "The config exists.\n\n[[radar:evidence=0]]",
588
+ evidence,
589
+ }}
590
+ collecting={false}
591
+ animateGroupIds={new Set()}
592
+ onViewSource={() => {}}
593
+ onViewActivity={() => {}}
594
+ />,
595
+ );
596
+ const start = html.indexOf("data-story-inventory-rows");
597
+ expect(start).toBeGreaterThan(-1);
598
+ // The inline block ends where the folded body (every row) begins.
599
+ const block = html.slice(start, html.indexOf('-body"', start));
600
+ expect(block).toContain("nginx-config");
601
+ expect(block).not.toContain("kube-root-ca.crt");
602
+ });
603
+
604
+ it("says when a cited entry is absent from the listing, or which entries a name could mean", () => {
605
+ expect(
606
+ namedInventoryRows(
607
+ [
608
+ { kind: "ConfigMap", namespace: "a", name: "config" },
609
+ { kind: "ConfigMap", namespace: "b", name: "config" },
610
+ ],
611
+ [
612
+ { subject: { kind: "ConfigMap", name: "config" } },
613
+ { subject: { kind: "ConfigMap", namespace: "b", name: "config" } },
614
+ { subject: { kind: "ConfigMap", name: "missing" } },
615
+ { subject: { kind: "ConfigMap", name: "missing" } },
616
+ ],
617
+ ),
618
+ ).toEqual([
619
+ { key: "/config", label: "config", matches: 2, row: undefined },
620
+ {
621
+ key: "b/config",
622
+ label: "b/config",
623
+ matches: 1,
624
+ row: { kind: "ConfigMap", namespace: "b", name: "config" },
625
+ },
626
+ { key: "/missing", label: "missing", matches: 0, row: undefined },
627
+ ]);
628
+ // Rows of a namespace-scoped listing omit their namespace: the scope is
629
+ // theirs, and only that namespace satisfies a namespaced citation.
630
+ const scoped = [{ kind: "ConfigMap", name: "config" }];
631
+ expect(
632
+ namedInventoryRows(
633
+ scoped,
634
+ [{ subject: { kind: "ConfigMap", namespace: "shop", name: "config" } }],
635
+ "shop",
636
+ )[0].matches,
637
+ ).toBe(1);
638
+ expect(
639
+ namedInventoryRows(
640
+ scoped,
641
+ [
642
+ {
643
+ subject: { kind: "ConfigMap", namespace: "other", name: "config" },
644
+ },
645
+ ],
646
+ "shop",
647
+ )[0].matches,
648
+ ).toBe(0);
649
+ expect(
650
+ namedInventoryRows(
651
+ [{ kind: "Namespace", name: "prod" }],
652
+ [{ subject: { kind: "Namespace", namespace: "other", name: "prod" } }],
653
+ )[0].matches,
654
+ ).toBe(0);
655
+ // A mixed listing holding a Service and a Deployment of one name shows
656
+ // the row of the kind the citation states.
657
+ expect(
658
+ namedInventoryRows(
659
+ [
660
+ { kind: "Service", namespace: "shop", name: "api" },
661
+ { kind: "Deployment", namespace: "shop", name: "api" },
662
+ ],
663
+ [{ subject: { kind: "Deployment", namespace: "shop", name: "api" } }],
664
+ )[0].row?.kind,
665
+ ).toBe("Deployment");
666
+ expect(
667
+ namedInventoryRows(
668
+ [{ kind: "APIService", name: "v1.apps" }],
669
+ [{ subject: { kind: "APIService", namespace: "", name: "v1.apps" } }],
670
+ )[0],
671
+ ).toEqual({
672
+ key: "/v1.apps",
673
+ label: "v1.apps",
674
+ matches: 1,
675
+ row: { kind: "APIService", name: "v1.apps" },
676
+ });
677
+ });
678
+
679
+ it("shows the chart inline on a metrics card placed as the symptom", () => {
680
+ const promRef = evidenceRef("m", "n");
681
+ const projection = projectInvestigationEvidence(
682
+ [
683
+ {
684
+ timeline: [
685
+ tool(
686
+ "diag",
687
+ "diagnose",
688
+ {
689
+ resource: {
690
+ apiVersion: "apps/v1",
691
+ kind: "Deployment",
692
+ metadata: { namespace: "shop", name: "api" },
693
+ spec: { replicas: 1 },
694
+ status: { replicas: 1, readyReplicas: 1 },
695
+ },
696
+ pods: 1,
697
+ podNames: ["api-1"],
698
+ events: [],
699
+ },
700
+ { evidenceRef: evidenceRef("o", "p") },
701
+ ),
702
+ tool(
703
+ "prom",
704
+ "query_prometheus",
705
+ {
706
+ query:
707
+ 'sum(container_memory_working_set_bytes{namespace="shop",pod=~"^(api-1)$"})',
708
+ type: "range",
709
+ start: "2026-09-06T07:17:23Z",
710
+ end: "2026-09-06T09:17:23Z",
711
+ step: "25s",
712
+ resultType: "matrix",
713
+ seriesCount: 1,
714
+ series: [
715
+ {
716
+ labels: { pod: "api-1" },
717
+ dataPoints: [
718
+ { timestamp: 1_788_679_043, value: 169_377_792 },
719
+ { timestamp: 1_788_679_068, value: 171_401_216 },
720
+ ],
721
+ },
722
+ ],
723
+ selectors: [
724
+ {
725
+ metric: "container_memory_working_set_bytes",
726
+ matchers: [
727
+ { label: "namespace", op: "=", value: "shop" },
728
+ { label: "pod", op: "=~", value: "^(api-1)$" },
729
+ ],
730
+ },
731
+ ],
732
+ },
733
+ { evidenceRef: promRef, summary: "{}" },
734
+ ),
735
+ ],
736
+ },
737
+ ],
738
+ target,
739
+ );
740
+ const evidence: DiagnosisEvidenceItem[] = [
741
+ {
742
+ status: "linked",
743
+ ref: promRef,
744
+ role: "symptom",
745
+ claim: "",
746
+ subject: {
747
+ kind: "Deployment",
748
+ namespace: "shop",
749
+ name: "api",
750
+ observation: "metrics",
751
+ },
752
+ },
753
+ ];
754
+ const html = renderToStaticMarkup(
755
+ <InvestigationEvidencePane
756
+ projection={projection}
757
+ investigationCase={resolveInvestigationCase(
758
+ projection,
759
+ { evidence },
760
+ 0,
761
+ )}
762
+ story={{ report: "Memory climbs.\n\n[[radar:evidence=0]]", evidence }}
763
+ collecting={false}
764
+ animateGroupIds={new Set()}
765
+ onViewSource={() => {}}
766
+ onViewActivity={() => {}}
767
+ />,
768
+ );
769
+ expect(html).toContain("data-story-metrics");
770
+ });
771
+ });