@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,632 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ investigationActivitySourceDomId,
4
+ investigationEvidenceSourceDomId,
5
+ investigationEvidenceSourceId,
6
+ investigationEvidenceStepIdsByTurn,
7
+ investigationEvidenceSubjectRef,
8
+ resolveInvestigationRootCauseEvidence,
9
+ type InvestigationEvidenceGroup,
10
+ } from "./index";
11
+ import {
12
+ criticalIssue,
13
+ deployment,
14
+ evidenceRef,
15
+ project,
16
+ tool,
17
+ warningEvent,
18
+ } from "./evidenceFixtures";
19
+
20
+ describe("investigation evidence source identity", () => {
21
+ it("is stable, DOM-safe, and shared by both pane anchors", () => {
22
+ const id = investigationEvidenceSourceId(2, "call/α:1");
23
+ expect(id).toBe("turn-2-step-call_x2f__x3b1__x3a_1");
24
+ expect(id).toMatch(/^[A-Za-z0-9_-]+$/);
25
+ expect(investigationActivitySourceDomId(id)).toBe(
26
+ `investigation-activity-${id}`,
27
+ );
28
+ expect(investigationEvidenceSourceDomId(id)).toBe(
29
+ `investigation-evidence-${id}`,
30
+ );
31
+ });
32
+
33
+ it("keeps escaped characters distinct from literal escape text", () => {
34
+ expect(investigationEvidenceSourceId(0, "/")).not.toBe(
35
+ investigationEvidenceSourceId(0, "_x2f_"),
36
+ );
37
+ expect(investigationEvidenceSourceId(0, "/")).toBe("turn-0-step-_x2f_");
38
+ expect(investigationEvidenceSourceId(0, "_x2f_")).toBe(
39
+ "turn-0-step-_x5f_x2f_x5f_",
40
+ );
41
+ });
42
+
43
+ it("keeps the empty token distinct from its literal sentinel", () => {
44
+ expect(investigationEvidenceSourceId(0, "")).not.toBe(
45
+ investigationEvidenceSourceId(0, "_empty_"),
46
+ );
47
+ expect(investigationEvidenceSourceId(0, "")).toBe("turn-0-step-_empty_");
48
+ expect(investigationEvidenceSourceId(0, "_empty_")).toBe(
49
+ "turn-0-step-_x5f_empty_x5f_",
50
+ );
51
+ expect(investigationEvidenceSourceId(0, "call-123")).toBe(
52
+ "turn-0-step-call-123",
53
+ );
54
+ });
55
+ });
56
+
57
+ describe("investigationEvidenceSubjectRef", () => {
58
+ it("uses resource identities stated by the captured evidence", () => {
59
+ expect(
60
+ investigationEvidenceSubjectRef({
61
+ type: "resource",
62
+ resource: {
63
+ apiVersion: "apps/v1",
64
+ kind: "Deployment",
65
+ metadata: { namespace: "shop", name: "api" },
66
+ },
67
+ warnings: [],
68
+ }),
69
+ ).toEqual({
70
+ kind: "Deployment",
71
+ group: "apps",
72
+ namespace: "shop",
73
+ name: "api",
74
+ });
75
+ expect(
76
+ investigationEvidenceSubjectRef({
77
+ type: "startup",
78
+ blocker: {
79
+ kind: "Pod",
80
+ name: "api-123",
81
+ reason: "ImagePullBackOff",
82
+ severity: "critical",
83
+ message: "The image could not be pulled.",
84
+ },
85
+ subject: { kind: "Pod", namespace: "shop", name: "api-123" },
86
+ }),
87
+ ).toEqual({ kind: "Pod", namespace: "shop", name: "api-123" });
88
+ expect(
89
+ investigationEvidenceSubjectRef({
90
+ type: "logs",
91
+ pod: "api-123",
92
+ container: "api",
93
+ namespace: "shop",
94
+ previous: false,
95
+ warnings: [],
96
+ }),
97
+ ).toEqual({ kind: "Pod", namespace: "shop", name: "api-123" });
98
+ expect(
99
+ investigationEvidenceSubjectRef({
100
+ type: "relationships",
101
+ root: { kind: "Service", namespace: "shop", name: "api" },
102
+ nodes: [],
103
+ edges: [],
104
+ truncated: false,
105
+ }),
106
+ ).toEqual({ kind: "Service", namespace: "shop", name: "api" });
107
+ expect(
108
+ investigationEvidenceSubjectRef({
109
+ type: "resource",
110
+ resource: {
111
+ apiVersion: "v1",
112
+ kind: "Node",
113
+ metadata: { name: "worker-1" },
114
+ },
115
+ warnings: [],
116
+ }),
117
+ ).toEqual({ kind: "Node", name: "worker-1" });
118
+ expect(
119
+ investigationEvidenceSubjectRef({
120
+ type: "resource",
121
+ resource: {
122
+ apiVersion: "v1",
123
+ kind: "Node",
124
+ metadata: { namespace: "kube-system", name: "worker-1" },
125
+ },
126
+ warnings: [],
127
+ }),
128
+ ).toBeUndefined();
129
+ });
130
+
131
+ it("does not invent a destination for ambiguous pod evidence", () => {
132
+ expect(
133
+ investigationEvidenceSubjectRef({
134
+ type: "logs",
135
+ pod: "api-123",
136
+ container: "api",
137
+ previous: false,
138
+ warnings: [],
139
+ }),
140
+ ).toBeUndefined();
141
+ expect(
142
+ investigationEvidenceSubjectRef({
143
+ type: "resource",
144
+ resource: {
145
+ apiVersion: "apps/v1",
146
+ kind: "Deployment",
147
+ metadata: { name: "api" },
148
+ },
149
+ warnings: [],
150
+ }),
151
+ ).toBeUndefined();
152
+ expect(
153
+ investigationEvidenceSubjectRef({
154
+ type: "crash",
155
+ crash: {
156
+ pods: ["api-1", "api-2"],
157
+ container: "api",
158
+ state: "waiting",
159
+ exitCode: 1,
160
+ logLine: "FATAL",
161
+ logSource: "current",
162
+ logLineSelection: "fatal_pattern",
163
+ },
164
+ namespace: "shop",
165
+ }),
166
+ ).toBeUndefined();
167
+ expect(
168
+ investigationEvidenceSubjectRef({
169
+ type: "receipt",
170
+ checked: "issues",
171
+ scope: "shop",
172
+ message: "No issues found",
173
+ }),
174
+ ).toBeUndefined();
175
+ });
176
+
177
+ it("suppresses missing namespaces only for known namespaced resources", () => {
178
+ expect(
179
+ investigationEvidenceSubjectRef({
180
+ type: "resource",
181
+ resource: {
182
+ apiVersion: "v1",
183
+ kind: "Secret",
184
+ metadata: { name: "api-token" },
185
+ },
186
+ warnings: [],
187
+ }),
188
+ ).toBeUndefined();
189
+ expect(
190
+ investigationEvidenceSubjectRef({
191
+ type: "resource",
192
+ resource: {
193
+ apiVersion: "example.io/v1",
194
+ kind: "Widget",
195
+ metadata: { name: "global-widget" },
196
+ },
197
+ warnings: [],
198
+ }),
199
+ ).toEqual({
200
+ kind: "Widget",
201
+ group: "example.io",
202
+ name: "global-widget",
203
+ });
204
+ expect(
205
+ investigationEvidenceSubjectRef({
206
+ type: "resource",
207
+ resource: {
208
+ apiVersion: "example.io/v1",
209
+ kind: "Deployment",
210
+ metadata: { name: "global-deployment" },
211
+ },
212
+ warnings: [],
213
+ }),
214
+ ).toEqual({
215
+ kind: "Deployment",
216
+ group: "example.io",
217
+ name: "global-deployment",
218
+ });
219
+ });
220
+
221
+ it("rejects malformed optional identity fields instead of creating a link", () => {
222
+ const malformed = [
223
+ {
224
+ type: "issue",
225
+ issue: { ...criticalIssue, namespace: { value: "shop" } },
226
+ },
227
+ {
228
+ type: "resource",
229
+ resource: {
230
+ ...deployment,
231
+ metadata: { ...deployment.metadata, namespace: ["shop"] },
232
+ },
233
+ warnings: [],
234
+ },
235
+ {
236
+ type: "network",
237
+ network: {
238
+ subject: {
239
+ kind: "Service",
240
+ namespace: 42,
241
+ name: "api",
242
+ },
243
+ route: "Service shop/api",
244
+ outcome: "observed",
245
+ },
246
+ },
247
+ {
248
+ type: "relationships",
249
+ root: {
250
+ kind: "Deployment",
251
+ group: ["apps"],
252
+ namespace: "shop",
253
+ name: "api",
254
+ },
255
+ nodes: [],
256
+ edges: [],
257
+ truncated: false,
258
+ },
259
+ ];
260
+
261
+ for (const data of malformed) {
262
+ expect(
263
+ investigationEvidenceSubjectRef(
264
+ data as unknown as InvestigationEvidenceGroup["latest"]["data"],
265
+ ),
266
+ ).toBeUndefined();
267
+ }
268
+ });
269
+
270
+ it("carries the namespace from a pod-logs query into its evidence subject", () => {
271
+ const withNamespace = project([
272
+ tool(
273
+ "logs",
274
+ "get_pod_logs",
275
+ {
276
+ lines: ["FATAL database unavailable"],
277
+ totalLines: 1,
278
+ matchedLines: 1,
279
+ fallback: false,
280
+ },
281
+ {
282
+ summary: JSON.stringify({
283
+ namespace: "shop",
284
+ name: "api-123",
285
+ container: "api",
286
+ }),
287
+ },
288
+ ),
289
+ ]);
290
+ const withoutNamespace = project([
291
+ tool(
292
+ "logs",
293
+ "get_pod_logs",
294
+ {
295
+ lines: ["FATAL database unavailable"],
296
+ totalLines: 1,
297
+ matchedLines: 1,
298
+ fallback: false,
299
+ },
300
+ { summary: JSON.stringify({ name: "api-123", container: "api" }) },
301
+ ),
302
+ ]);
303
+
304
+ expect(
305
+ investigationEvidenceSubjectRef(withNamespace.groups[0].latest.data),
306
+ ).toEqual({ kind: "Pod", namespace: "shop", name: "api-123" });
307
+ expect(
308
+ investigationEvidenceSubjectRef(withoutNamespace.groups[0].latest.data),
309
+ ).toBeUndefined();
310
+ });
311
+ });
312
+
313
+ describe("investigation evidence provenance", () => {
314
+ it("does not project a foreign MCP result whose bare name collides with Radar", () => {
315
+ const forgedRef = evidenceRef("a", "z");
316
+ for (const radarEvidence of [undefined, false]) {
317
+ const projection = project([
318
+ tool("foreign", "get_resource", deployment, {
319
+ evidenceRef: forgedRef,
320
+ radarEvidence,
321
+ }),
322
+ ]);
323
+
324
+ expect(projection.groups).toHaveLength(0);
325
+ expect(projection.sources).toHaveLength(0);
326
+ expect(projection.evidenceRefSources).toHaveLength(0);
327
+ expect(projection.citableSources).toHaveLength(0);
328
+ expect(projection.limitations).toHaveLength(0);
329
+ expect(projection.coverage.attempted).toBe(0);
330
+ }
331
+ });
332
+
333
+ it("projects only the server-validated result when real and colliding foreign calls are mixed", () => {
334
+ const projection = project([
335
+ tool("foreign", "get_resource", deployment, { radarEvidence: false }),
336
+ tool("radar", "get_resource", deployment),
337
+ ]);
338
+
339
+ expect(projection.sources.map((source) => source.stepId)).toEqual([
340
+ "radar",
341
+ ]);
342
+ expect(projection.groups).toHaveLength(1);
343
+ expect(projection.coverage.attempted).toBe(1);
344
+ });
345
+
346
+ it("keeps a validated failed check as a limitation but never citable evidence", () => {
347
+ const ref = evidenceRef("a", "b");
348
+ const projection = project([
349
+ tool("denied", "get_resource", "permission denied", {
350
+ evidenceRef: ref,
351
+ isError: true,
352
+ }),
353
+ ]);
354
+
355
+ expect(projection.groups).toHaveLength(0);
356
+ expect(projection.sources.map((source) => source.stepId)).toEqual([
357
+ "denied",
358
+ ]);
359
+ expect(projection.evidenceRefSources).toHaveLength(1);
360
+ expect(projection.citableSources).toHaveLength(0);
361
+ expect(projection.limitations).toEqual([
362
+ expect.objectContaining({
363
+ source: "Resource details",
364
+ kind: "error",
365
+ message: "permission denied",
366
+ }),
367
+ ]);
368
+ });
369
+
370
+ it("surfaces a validated truncated result as a limit without trusting its retained payload", () => {
371
+ const ref = evidenceRef("a", "b");
372
+ const validated = project([
373
+ tool("partial", "get_resource", '{"resource":', {
374
+ evidenceRef: ref,
375
+ truncated: true,
376
+ }),
377
+ ]);
378
+
379
+ expect(validated.sources.map((source) => source.stepId)).toEqual([
380
+ "partial",
381
+ ]);
382
+ expect(validated.groups).toHaveLength(0);
383
+ expect(validated.citableSources).toHaveLength(0);
384
+ expect(validated.limitations).toEqual([
385
+ expect.objectContaining({
386
+ source: "Resource details",
387
+ kind: "truncated",
388
+ }),
389
+ ]);
390
+
391
+ const unvalidated = project([
392
+ tool("foreign-partial", "get_resource", '{"resource":', {
393
+ evidenceRef: ref,
394
+ radarEvidence: false,
395
+ truncated: true,
396
+ }),
397
+ ]);
398
+ expect(unvalidated.sources).toHaveLength(0);
399
+ expect(unvalidated.groups).toHaveLength(0);
400
+ expect(unvalidated.citableSources).toHaveLength(0);
401
+ expect(unvalidated.limitations).toHaveLength(0);
402
+ });
403
+ });
404
+
405
+ describe("root-cause evidence resolution", () => {
406
+ it("resolves server-linked refs in model order and snapshots the exact typed source", () => {
407
+ const firstRef = evidenceRef("a", "b");
408
+ const secondRef = evidenceRef("a", "c");
409
+ const projection = project([
410
+ tool(
411
+ "diagnose-1",
412
+ "diagnose",
413
+ {
414
+ resource: deployment,
415
+ resourceContext: { tier: "basic" },
416
+ relatedIssues: [criticalIssue],
417
+ },
418
+ { evidenceRef: firstRef },
419
+ ),
420
+ tool(
421
+ "events-1",
422
+ "get_events",
423
+ { events: [warningEvent] },
424
+ { evidenceRef: secondRef },
425
+ ),
426
+ ]);
427
+
428
+ const resolution = resolveInvestigationRootCauseEvidence(
429
+ projection,
430
+ { status: "linked", refs: [secondRef, firstRef] },
431
+ 0,
432
+ );
433
+
434
+ expect(resolution.status).toBe("linked");
435
+ expect(resolution.links.map((link) => link.source.stepId)).toEqual([
436
+ "events-1",
437
+ "diagnose-1",
438
+ ]);
439
+ expect(resolution.links.every((link) => link.originalGroupId)).toBe(true);
440
+ expect(resolution.links[1].originalGroupId).toBe(
441
+ projection.sources.find((source) => source.stepId === "diagnose-1")
442
+ ?.primaryGroupId,
443
+ );
444
+ });
445
+
446
+ it("keeps an unadapted successful Radar check linkable to Activity", () => {
447
+ const ref = evidenceRef("a", "b");
448
+ const projection = project([
449
+ tool(
450
+ "metrics-1",
451
+ "discover_metrics",
452
+ { result: [1] },
453
+ { evidenceRef: ref },
454
+ ),
455
+ ]);
456
+
457
+ expect(projection.groups).toHaveLength(0);
458
+ expect(projection.citableSources).toHaveLength(1);
459
+ const resolution = resolveInvestigationRootCauseEvidence(
460
+ projection,
461
+ { status: "linked", refs: [ref] },
462
+ 0,
463
+ );
464
+ expect(resolution).toMatchObject({
465
+ status: "linked",
466
+ links: [{ source: { stepId: "metrics-1" } }],
467
+ });
468
+ expect(resolution.links[0].originalGroupId).toBeUndefined();
469
+ expect(
470
+ investigationEvidenceStepIdsByTurn(projection).get(0),
471
+ ).toBeUndefined();
472
+ });
473
+
474
+ it("fails closed for malformed, unmatched, later-turn, failed, or partial refs", () => {
475
+ const currentRef = evidenceRef("a", "b");
476
+ const priorRef = evidenceRef("c", "d");
477
+ const failedRef = evidenceRef("a", "e");
478
+ const partialRef = evidenceRef("a", "f");
479
+ const projection = project(
480
+ [tool("old", "get_resource", deployment, { evidenceRef: priorRef })],
481
+ [
482
+ tool("current", "get_resource", deployment, {
483
+ evidenceRef: currentRef,
484
+ }),
485
+ tool("failed", "get_resource", deployment, {
486
+ evidenceRef: failedRef,
487
+ isError: true,
488
+ }),
489
+ tool("partial", "get_resource", deployment, {
490
+ evidenceRef: partialRef,
491
+ truncated: true,
492
+ }),
493
+ ],
494
+ );
495
+
496
+ // A result read in an earlier turn of the same run is citable — a revised
497
+ // assessment may rest on it — and resolves to that earlier read.
498
+ expect(
499
+ resolveInvestigationRootCauseEvidence(
500
+ projection,
501
+ { status: "linked", refs: [priorRef] },
502
+ 1,
503
+ ),
504
+ ).toMatchObject({ status: "linked", links: [{ source: { stepId: "old" } }] });
505
+ // A result read AFTER the assessment turn is not: the assessment could not
506
+ // have seen it.
507
+ expect(
508
+ resolveInvestigationRootCauseEvidence(
509
+ projection,
510
+ { status: "linked", refs: [currentRef] },
511
+ 0,
512
+ ),
513
+ ).toEqual({ status: "invalid", links: [] });
514
+
515
+ for (const evidence of [
516
+ { status: "linked" as const, refs: [evidenceRef("a", "z")] },
517
+ { status: "linked" as const, refs: [failedRef] },
518
+ { status: "linked" as const, refs: [partialRef] },
519
+ { status: "linked" as const, refs: [currentRef, currentRef] },
520
+ { status: "linked" as const, refs: [] },
521
+ { status: "linked" as const, refs: ["invalid"] },
522
+ ]) {
523
+ expect(
524
+ resolveInvestigationRootCauseEvidence(projection, evidence, 1),
525
+ ).toEqual({ status: "invalid", links: [] });
526
+ }
527
+ expect(
528
+ resolveInvestigationRootCauseEvidence(
529
+ projection,
530
+ { status: "linked", refs: [currentRef, evidenceRef("a", "z")] },
531
+ 1,
532
+ ),
533
+ ).toEqual({ status: "invalid", links: [] });
534
+
535
+ const duplicateSourceProjection = project([
536
+ tool("first", "get_resource", deployment, {
537
+ evidenceRef: currentRef,
538
+ }),
539
+ tool("second", "get_resource", deployment, {
540
+ evidenceRef: currentRef,
541
+ }),
542
+ ]);
543
+ expect(
544
+ resolveInvestigationRootCauseEvidence(
545
+ duplicateSourceProjection,
546
+ { status: "linked", refs: [currentRef] },
547
+ 0,
548
+ ),
549
+ ).toEqual({ status: "invalid", links: [] });
550
+ });
551
+
552
+ it("counts duplicate refs across every turn up to the assessment", () => {
553
+ const ref = evidenceRef("a", "b");
554
+ const currentDuplicate = project(
555
+ [tool("prior", "get_resource", deployment, { evidenceRef: ref })],
556
+ [
557
+ tool("current", "get_resource", deployment, { evidenceRef: ref }),
558
+ tool("failed-duplicate", "get_resource", deployment, {
559
+ evidenceRef: ref,
560
+ isError: true,
561
+ }),
562
+ ],
563
+ );
564
+ expect(currentDuplicate.evidenceRefSources).toHaveLength(3);
565
+ expect(
566
+ resolveInvestigationRootCauseEvidence(
567
+ currentDuplicate,
568
+ { status: "linked", refs: [ref] },
569
+ 1,
570
+ ),
571
+ ).toEqual({ status: "invalid", links: [] });
572
+
573
+ const priorDuplicateOnly = project(
574
+ [
575
+ tool("prior-first", "get_resource", deployment, { evidenceRef: ref }),
576
+ tool("prior-second", "get_resource", deployment, {
577
+ evidenceRef: ref,
578
+ isError: true,
579
+ }),
580
+ ],
581
+ [tool("current-only", "get_resource", deployment, { evidenceRef: ref })],
582
+ );
583
+ // A server-issued ref names one payload in one scope; the same ref in two
584
+ // turns can only be a replay, so run-wide uniqueness is the rule — the
585
+ // same rule the server binds by.
586
+ expect(
587
+ resolveInvestigationRootCauseEvidence(
588
+ priorDuplicateOnly,
589
+ { status: "linked", refs: [ref] },
590
+ 1,
591
+ ),
592
+ ).toEqual({ status: "invalid", links: [] });
593
+ });
594
+
595
+ it("rejects a replay that claims refs from different investigation scopes are linked", () => {
596
+ const firstRef = evidenceRef("a", "b");
597
+ const secondRef = evidenceRef("c", "d");
598
+ const projection = project([
599
+ tool("first", "get_resource", deployment, { evidenceRef: firstRef }),
600
+ tool("second", "get_resource", deployment, { evidenceRef: secondRef }),
601
+ ]);
602
+
603
+ expect(
604
+ resolveInvestigationRootCauseEvidence(
605
+ projection,
606
+ { status: "linked", refs: [firstRef, secondRef] },
607
+ 0,
608
+ ),
609
+ ).toEqual({ status: "invalid", links: [] });
610
+ });
611
+
612
+ it("preserves explicit missing and invalid server states without inventing links", () => {
613
+ const projection = project([]);
614
+ expect(
615
+ resolveInvestigationRootCauseEvidence(projection, undefined, 0),
616
+ ).toEqual({ status: "missing", links: [] });
617
+ expect(
618
+ resolveInvestigationRootCauseEvidence(
619
+ projection,
620
+ { status: "missing" },
621
+ 0,
622
+ ),
623
+ ).toEqual({ status: "missing", links: [] });
624
+ expect(
625
+ resolveInvestigationRootCauseEvidence(
626
+ projection,
627
+ { status: "invalid" },
628
+ 0,
629
+ ),
630
+ ).toEqual({ status: "invalid", links: [] });
631
+ });
632
+ });