@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
@@ -14,6 +14,7 @@ import {
14
14
  investigationClosedEventIsLive,
15
15
  investigationClosedRunIsUnavailable,
16
16
  investigationEvidenceInputsEqual,
17
+ investigationEvidenceCoverageGaps,
17
18
  investigationEvidenceCoverageLimited,
18
19
  investigationEvidenceConflictsWithHealthy,
19
20
  investigationHealthConflictExplainedBy,
@@ -22,7 +23,10 @@ import {
22
23
  investigationInteractionsBlocked,
23
24
  investigationIsReadOnly,
24
25
  investigationPaneCenteredScrollTop,
25
- investigationLiveCaseTurnIndex,
26
+ investigationAssessmentTurnIndexes,
27
+ investigationHealthSignals,
28
+ investigationSettledAnswerTurnIndexes,
29
+ investigationIsAssessmentTurn,
26
30
  } from "./investigationState";
27
31
 
28
32
  describe("investigation terminal presentation", () => {
@@ -169,6 +173,38 @@ describe("investigation terminal presentation", () => {
169
173
  });
170
174
 
171
175
  describe("investigation evidence projection stability", () => {
176
+ it("does not call a resource read partial when Diagnose cannot bundle the kind", () => {
177
+ const projection = {
178
+ limitations: [],
179
+ coverage: { attempted: 1, projected: 1, limited: 0, checked: 0 },
180
+ sources: [
181
+ { id: "resource", tool: "get_resource", confirmedSuccess: true },
182
+ ],
183
+ groups: [
184
+ {
185
+ latest: { relevance: "target" as const, source: { id: "resource" } },
186
+ },
187
+ ],
188
+ };
189
+ expect(
190
+ investigationEvidenceCoverageGaps(projection, "HorizontalPodAutoscaler"),
191
+ ).toEqual({ noEvidence: false, noTargetDiagnosis: false });
192
+ expect(
193
+ investigationEvidenceCoverageLimited(
194
+ projection,
195
+ "HorizontalPodAutoscaler",
196
+ ),
197
+ ).toBe(false);
198
+ expect(investigationEvidenceCoverageGaps(projection, "Deployment")).toEqual(
199
+ {
200
+ noEvidence: false,
201
+ noTargetDiagnosis: true,
202
+ },
203
+ );
204
+ expect(investigationEvidenceCoverageLimited(projection, "Deployment")).toBe(
205
+ true,
206
+ );
207
+ });
172
208
  it("treats zero projected producer evidence as limited coverage", () => {
173
209
  expect(
174
210
  investigationEvidenceCoverageLimited({
@@ -817,85 +853,71 @@ describe("investigation action gating", () => {
817
853
  });
818
854
  });
819
855
 
820
- describe("investigationLiveCaseTurnIndex", () => {
821
- const linkedItem = {
822
- status: "linked" as const,
823
- ref: "ev_x",
824
- role: "context" as const,
825
- claim: "c",
826
- };
856
+ describe("investigationAssessmentTurnIndexes", () => {
827
857
  const assessment = {
828
858
  status: "done" as const,
829
859
  diagnosis: { healthy: true, rootCause: "", report: "", remediation: [] },
830
860
  };
831
- it("keeps the assessment when no later turn cites evidence", () => {
832
- const answer = {
861
+ it("keeps the assessment across answers that only restate a root cause", () => {
862
+ // Agents restate the cause on most answers; without an explicit revision
863
+ // signal every question would rewrite Findings.
864
+ const restating = {
833
865
  status: "done" as const,
834
- question: "why?",
835
- diagnosis: { rootCause: "", report: "because", remediation: [] },
836
- };
837
- expect(investigationLiveCaseTurnIndex([assessment, answer], 0)).toBe(0);
838
- });
839
- it("moves to an answer turn that carries a bound case or linked root-cause refs", () => {
840
- const cited = {
841
- status: "done" as const,
842
- question: "chart it and cite it",
866
+ question: "get the secret",
843
867
  diagnosis: {
844
- rootCause: "",
868
+ rootCause: "Secret x does not exist",
845
869
  report: "",
846
870
  remediation: [],
847
- evidence: [linkedItem],
848
871
  },
849
872
  };
850
- expect(investigationLiveCaseTurnIndex([assessment, cited], 0)).toBe(1);
851
- const legacy = {
873
+ expect(investigationAssessmentTurnIndexes([assessment, restating])).toEqual(
874
+ [0],
875
+ );
876
+ expect(investigationIsAssessmentTurn(restating)).toBe(false);
877
+ });
878
+ it("accepts a question turn only when it revises with a complete verdict", () => {
879
+ const revised = {
852
880
  status: "done" as const,
853
- question: "what broke?",
881
+ question: "could it be OOM?",
854
882
  diagnosis: {
855
- rootCause: "x",
883
+ rootCause: "The container is OOM-killed.",
884
+ summary: "The app runs out of memory.",
885
+ revisesAssessment: true,
856
886
  report: "",
857
887
  remediation: [],
858
- rootCauseEvidence: { status: "linked" as const, refs: ["ev_x"] },
859
888
  },
860
889
  };
861
- expect(investigationLiveCaseTurnIndex([assessment, legacy], 0)).toBe(1);
862
- const unlinkedOnly = {
863
- ...cited,
864
- diagnosis: {
865
- ...cited.diagnosis,
866
- evidence: [{ status: "unlinked" as const }],
867
- },
890
+ expect(investigationAssessmentTurnIndexes([assessment, revised])).toEqual([
891
+ 0, 1,
892
+ ]);
893
+ const flagOnly = {
894
+ ...revised,
895
+ diagnosis: { ...revised.diagnosis, summary: undefined },
868
896
  };
869
- expect(investigationLiveCaseTurnIndex([assessment, unlinkedOnly], 0)).toBe(
870
- 0,
871
- );
872
- });
873
- it("ignores apply, explanation, running, and superseded turns", () => {
874
- const cited = {
875
- status: "done" as const,
876
- question: "cite",
897
+ expect(investigationIsAssessmentTurn(flagOnly)).toBe(false);
898
+ const noVerdict = {
899
+ ...revised,
877
900
  diagnosis: {
878
901
  rootCause: "",
902
+ summary: "Words.",
903
+ revisesAssessment: true,
879
904
  report: "",
880
- remediation: [],
881
- evidence: [linkedItem],
905
+ remediation: ["x"],
882
906
  },
883
907
  };
884
- expect(
885
- investigationLiveCaseTurnIndex(
886
- [
887
- assessment,
888
- { ...cited, apply: true },
889
- { ...cited, explainAssessment: 2 },
890
- { ...cited, status: "running" as const },
891
- ],
892
- 0,
893
- ),
894
- ).toBe(0);
895
- // A newer assessment after the cited answer is the current one.
896
- expect(
897
- investigationLiveCaseTurnIndex([assessment, cited, assessment], 2),
898
- ).toBe(2);
908
+ expect(investigationIsAssessmentTurn(noVerdict)).toBe(false);
909
+ });
910
+ it("always takes verifications and never apply, explanation or running turns", () => {
911
+ const verify = { ...assessment, question: "re-check", verify: true };
912
+ expect(
913
+ investigationAssessmentTurnIndexes([
914
+ assessment,
915
+ { ...assessment, apply: true },
916
+ { ...assessment, explainAssessment: 2 },
917
+ { ...assessment, status: "running" as const },
918
+ verify,
919
+ ]),
920
+ ).toEqual([0, 4]);
899
921
  });
900
922
  });
901
923
 
@@ -1015,3 +1037,239 @@ describe("adverse evidence and the healthy-conflict banner", () => {
1015
1037
  }
1016
1038
  });
1017
1039
  });
1040
+
1041
+ describe("investigationSettledAnswerTurnIndexes", () => {
1042
+ const assessment = {
1043
+ status: "done" as const,
1044
+ diagnosis: { rootCause: "x", report: "s", remediation: [], summary: "S." },
1045
+ };
1046
+ const answer = (revises?: boolean) => ({
1047
+ status: "done" as const,
1048
+ question: "q",
1049
+ diagnosis: {
1050
+ rootCause: "",
1051
+ report: "a",
1052
+ remediation: [],
1053
+ revisesAssessment: revises,
1054
+ },
1055
+ });
1056
+ it("settles non-revising answers under the story contract only", () => {
1057
+ expect(
1058
+ investigationSettledAnswerTurnIndexes(
1059
+ [
1060
+ assessment,
1061
+ answer(false),
1062
+ answer(true),
1063
+ { ...answer(), verify: true },
1064
+ ],
1065
+ 0,
1066
+ ),
1067
+ ).toEqual(new Set([1]));
1068
+ const legacy = {
1069
+ ...assessment,
1070
+ diagnosis: { ...assessment.diagnosis, summary: undefined },
1071
+ };
1072
+ expect(
1073
+ investigationSettledAnswerTurnIndexes([legacy, answer(false)], 0),
1074
+ ).toEqual(new Set());
1075
+ });
1076
+ });
1077
+
1078
+ describe("investigationHealthSignals", () => {
1079
+ const adverse = (id: string, title: string) => ({
1080
+ id,
1081
+ identity: id,
1082
+ historical: false,
1083
+ kind: "issue",
1084
+ latest: {
1085
+ relevance: "target" as const,
1086
+ tier: "key" as const,
1087
+ tone: "warning",
1088
+ title,
1089
+ source: { turnIndex: 0, id: `src-${id}` },
1090
+ },
1091
+ });
1092
+ it("classifies each adverse card by the agent's position on it", () => {
1093
+ const projection = {
1094
+ groups: [
1095
+ adverse("a", "Readiness probe failing"),
1096
+ adverse("b", "Restarts"),
1097
+ adverse("c", "OOM"),
1098
+ adverse("d", "Evicted"),
1099
+ ],
1100
+ };
1101
+ const items = [
1102
+ {
1103
+ role: "benign",
1104
+ placement: "card" as const,
1105
+ claim: "Timeouts never removed it from endpoints.",
1106
+ groupId: "a",
1107
+ source: { turnIndex: 0 },
1108
+ },
1109
+ {
1110
+ role: "demoted",
1111
+ placement: "card" as const,
1112
+ claim: "Old restarts.",
1113
+ groupId: "b",
1114
+ source: { turnIndex: 0 },
1115
+ },
1116
+ {
1117
+ role: "symptom",
1118
+ placement: "card" as const,
1119
+ claim: "",
1120
+ groupId: "c",
1121
+ source: { turnIndex: 0 },
1122
+ },
1123
+ ];
1124
+ expect(
1125
+ investigationHealthSignals(projection, items).map((s) => [
1126
+ s.title,
1127
+ s.status,
1128
+ ]),
1129
+ ).toEqual([
1130
+ ["Readiness probe failing", "explained"],
1131
+ ["Restarts", "related"],
1132
+ ["OOM", "contradiction"],
1133
+ ["Evicted", "unaddressed"],
1134
+ ]);
1135
+ });
1136
+ });
1137
+
1138
+ describe("investigationHealthSignals twins across scopes", () => {
1139
+ const warning = {
1140
+ reason: "Unhealthy",
1141
+ message:
1142
+ "Readiness probe failed: check failed http://localhost:9898/readyz",
1143
+ type: "Warning",
1144
+ };
1145
+ const eventsGroup = (
1146
+ id: string,
1147
+ identity: string,
1148
+ patch: {
1149
+ relevance?: "target" | "producer-related" | "broader";
1150
+ events?: (typeof warning)[];
1151
+ turnIndex?: number;
1152
+ } = {},
1153
+ ) => ({
1154
+ id,
1155
+ identity,
1156
+ historical: false,
1157
+ kind: "events",
1158
+ latest: {
1159
+ relevance: patch.relevance ?? ("target" as const),
1160
+ tier: "key" as const,
1161
+ tone: "warning",
1162
+ title: "Kubernetes events",
1163
+ summary: `${warning.reason}: ${warning.message} · ${identity}`,
1164
+ data: { type: "events", events: patch.events ?? [warning] },
1165
+ source: { turnIndex: patch.turnIndex ?? 0, id },
1166
+ },
1167
+ });
1168
+ const benignOnPod = [
1169
+ {
1170
+ role: "benign",
1171
+ placement: "card" as const,
1172
+ claim:
1173
+ "Probe timeouts during a node stall; the pod has been Ready since.",
1174
+ groupId: "events-pod",
1175
+ source: { turnIndex: 0 },
1176
+ },
1177
+ ];
1178
+
1179
+ it("lets a benign note on the Pod's copy of a warning explain the Deployment's copy", () => {
1180
+ const groups = [
1181
+ eventsGroup("events-deploy", "deployment demo/podinfo"),
1182
+ eventsGroup("events-pod", "pod demo/podinfo-676569b68b-6278c"),
1183
+ ];
1184
+ expect(
1185
+ investigationHealthSignals({ groups }, benignOnPod).map((signal) => [
1186
+ signal.groupId,
1187
+ signal.status,
1188
+ ]),
1189
+ ).toEqual([
1190
+ ["events-deploy", "explained"],
1191
+ ["events-pod", "explained"],
1192
+ ]);
1193
+ expect(
1194
+ investigationHealthConflictExplainedBy({ groups }, benignOnPod),
1195
+ ).toEqual(["Kubernetes events", "Kubernetes events"]);
1196
+ });
1197
+
1198
+ it("does not let an earlier turn's note on the Pod explain the Deployment's copy captured later", () => {
1199
+ const groups = [
1200
+ eventsGroup("events-deploy", "deployment demo/podinfo", { turnIndex: 1 }),
1201
+ eventsGroup("events-pod", "pod demo/podinfo-676569b68b-6278c"),
1202
+ ];
1203
+ expect(
1204
+ investigationHealthSignals({ groups }, benignOnPod).map((signal) => [
1205
+ signal.groupId,
1206
+ signal.status,
1207
+ ]),
1208
+ ).toEqual([
1209
+ ["events-deploy", "unaddressed"],
1210
+ ["events-pod", "explained"],
1211
+ ]);
1212
+ });
1213
+
1214
+ it("never twins a cluster-scoped card with a namespaced one", () => {
1215
+ const groups = [
1216
+ eventsGroup("events-node", "node worker-1"),
1217
+ eventsGroup("events-pod", "pod demo/podinfo-676569b68b-6278c"),
1218
+ ];
1219
+ expect(
1220
+ investigationHealthSignals({ groups }, benignOnPod).map((signal) => [
1221
+ signal.groupId,
1222
+ signal.status,
1223
+ ]),
1224
+ ).toEqual([
1225
+ ["events-node", "unaddressed"],
1226
+ ["events-pod", "explained"],
1227
+ ]);
1228
+ });
1229
+
1230
+ it("does not let a card with one warning explain a card that carries that warning and another", () => {
1231
+ const extra = {
1232
+ reason: "BackOff",
1233
+ message: "Back-off restarting failed container",
1234
+ type: "Warning",
1235
+ };
1236
+ const groups = [
1237
+ eventsGroup("events-deploy", "deployment demo/podinfo", {
1238
+ events: [warning, extra],
1239
+ }),
1240
+ eventsGroup("events-pod", "pod demo/podinfo-676569b68b-6278c"),
1241
+ ];
1242
+ expect(
1243
+ investigationHealthSignals({ groups }, benignOnPod).map((signal) => [
1244
+ signal.groupId,
1245
+ signal.status,
1246
+ ]),
1247
+ ).toEqual([
1248
+ ["events-deploy", "unaddressed"],
1249
+ ["events-pod", "explained"],
1250
+ ]);
1251
+ });
1252
+
1253
+ it("keeps a different warning, another namespace, or a broader card as its own signal", () => {
1254
+ const groups = [
1255
+ eventsGroup("events-deploy", "deployment demo/podinfo", {
1256
+ events: [{ ...warning, message: "Liveness probe failed: timeout" }],
1257
+ }),
1258
+ eventsGroup("events-elsewhere", "deployment other/podinfo"),
1259
+ eventsGroup("events-broader", "deployment demo/frontend", {
1260
+ relevance: "broader",
1261
+ }),
1262
+ eventsGroup("events-pod", "pod demo/podinfo-676569b68b-6278c"),
1263
+ ];
1264
+ expect(
1265
+ investigationHealthSignals({ groups }, benignOnPod).map((signal) => [
1266
+ signal.groupId,
1267
+ signal.status,
1268
+ ]),
1269
+ ).toEqual([
1270
+ ["events-deploy", "unaddressed"],
1271
+ ["events-elsewhere", "unaddressed"],
1272
+ ["events-pod", "explained"],
1273
+ ]);
1274
+ });
1275
+ });