@skyhook-io/radar-app 1.13.4 → 1.14.0

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 (230) hide show
  1. package/package.json +8 -8
  2. package/src/App.tsx +60 -24
  3. package/src/RadarApp.tsx +5 -0
  4. package/src/api/client.authRedirect.test.ts +12 -1
  5. package/src/api/client.prometheusStatus.test.ts +24 -0
  6. package/src/api/client.ts +325 -16
  7. package/src/api/diagnose.ts +62 -15
  8. package/src/api/drain-plan.test.ts +86 -0
  9. package/src/api/quotas.test.ts +26 -0
  10. package/src/api/quotas.ts +20 -0
  11. package/src/api/workloadMetrics.test.ts +40 -0
  12. package/src/api/workloadMetrics.ts +68 -0
  13. package/src/components/CloudConnectFlow.tsx +179 -42
  14. package/src/components/CloudFunnelButton.tsx +107 -36
  15. package/src/components/ConnectionErrorView.test.tsx +30 -0
  16. package/src/components/ConnectionErrorView.tsx +31 -19
  17. package/src/components/SyncProgressPanel.tsx +73 -0
  18. package/src/components/audit/AuditView.tsx +2 -0
  19. package/src/components/cloudConnectHandoff.test.ts +140 -0
  20. package/src/components/cloudConnectHandoff.ts +124 -0
  21. package/src/components/diagnose/AgentCase.tsx +131 -0
  22. package/src/components/diagnose/DiagnoseContext.tsx +2 -1
  23. package/src/components/diagnose/DiagnoseSurface.test.tsx +0 -20
  24. package/src/components/diagnose/DiagnoseSurface.tsx +32 -195
  25. package/src/components/diagnose/InvestigationEvidencePane.test.tsx +783 -7
  26. package/src/components/diagnose/InvestigationEvidencePane.tsx +774 -133
  27. package/src/components/diagnose/InvestigationView.tsx +222 -5
  28. package/src/components/diagnose/diagnoseEvidenceTypes.ts +22 -1
  29. package/src/components/diagnose/investigationCase.test.tsx +1568 -0
  30. package/src/components/diagnose/investigationCase.ts +439 -0
  31. package/src/components/diagnose/investigationEvidence/adapters/changes.ts +60 -0
  32. package/src/components/diagnose/investigationEvidence/adapters/diagnose.test.ts +573 -0
  33. package/src/components/diagnose/investigationEvidence/adapters/diagnose.ts +935 -0
  34. package/src/components/diagnose/investigationEvidence/adapters/events.ts +96 -0
  35. package/src/components/diagnose/investigationEvidence/adapters/helm.test.ts +242 -0
  36. package/src/components/diagnose/investigationEvidence/adapters/helm.ts +208 -0
  37. package/src/components/diagnose/investigationEvidence/adapters/issues.ts +172 -0
  38. package/src/components/diagnose/investigationEvidence/adapters/logs.test.ts +283 -0
  39. package/src/components/diagnose/investigationEvidence/adapters/logs.ts +183 -0
  40. package/src/components/diagnose/investigationEvidence/adapters/permissions.test.ts +528 -0
  41. package/src/components/diagnose/investigationEvidence/adapters/permissions.ts +292 -0
  42. package/src/components/diagnose/investigationEvidence/adapters/prometheus.test.ts +1641 -0
  43. package/src/components/diagnose/investigationEvidence/adapters/prometheus.ts +769 -0
  44. package/src/components/diagnose/investigationEvidence/adapters/resource.test.ts +54 -0
  45. package/src/components/diagnose/investigationEvidence/adapters/resource.ts +257 -0
  46. package/src/components/diagnose/investigationEvidence/adapters/topology.ts +207 -0
  47. package/src/components/diagnose/investigationEvidence/builder.ts +418 -0
  48. package/src/components/diagnose/investigationEvidence/evidenceFixtures.ts +179 -0
  49. package/src/components/diagnose/investigationEvidence/identity.test.ts +614 -0
  50. package/src/components/diagnose/investigationEvidence/identity.ts +261 -0
  51. package/src/components/diagnose/investigationEvidence/index.ts +55 -0
  52. package/src/components/diagnose/investigationEvidence/observations.ts +954 -0
  53. package/src/components/diagnose/investigationEvidence/parse.ts +467 -0
  54. package/src/components/diagnose/investigationEvidence/projection.test.ts +2703 -0
  55. package/src/components/diagnose/investigationEvidence/types.ts +512 -0
  56. package/src/components/diagnose/investigationEvidenceKinds.ts +218 -0
  57. package/src/components/diagnose/investigationEvidencePresentation.test.ts +31 -0
  58. package/src/components/diagnose/investigationEvidencePresentation.ts +1 -0
  59. package/src/components/diagnose/investigationMetrics.test.ts +712 -0
  60. package/src/components/diagnose/investigationMetrics.ts +393 -0
  61. package/src/components/diagnose/investigationSourceFocus.ts +2 -0
  62. package/src/components/diagnose/investigationState.test.ts +322 -0
  63. package/src/components/diagnose/investigationState.ts +147 -25
  64. package/src/components/diagnose/parts.test.tsx +482 -0
  65. package/src/components/diagnose/parts.tsx +418 -41
  66. package/src/components/diagnose/target.test.ts +19 -0
  67. package/src/components/diagnose/target.ts +3 -2
  68. package/src/components/gitops/GitOpsView.tsx +7 -0
  69. package/src/components/gitops/RemoteDestinationCloudHint.tsx +26 -0
  70. package/src/components/resource/PrometheusCharts.overlays.test.ts +22 -0
  71. package/src/components/resource/PrometheusCharts.tsx +4 -3
  72. package/src/components/resource/PrometheusChartsGrid.render.test.tsx +99 -0
  73. package/src/components/resource/PrometheusChartsGrid.tsx +245 -103
  74. package/src/components/resource/RightsizingStrip.test.ts +38 -0
  75. package/src/components/resource/RightsizingStrip.tsx +15 -3
  76. package/src/components/resource/WorkloadMetricsHelpDialog.test.tsx +79 -0
  77. package/src/components/resource/WorkloadMetricsHelpDialog.tsx +145 -0
  78. package/src/components/resource/WorkloadMetricsSection.render.test.tsx +351 -0
  79. package/src/components/resource/WorkloadMetricsSection.tsx +486 -0
  80. package/src/components/resource/workloadMetricValues.test.ts +65 -0
  81. package/src/components/resource/workloadMetricValues.ts +30 -0
  82. package/src/components/resources/PodFilePreview.test.tsx +131 -0
  83. package/src/components/resources/PodFilePreview.tsx +394 -0
  84. package/src/components/resources/PodFilesystemModal.tsx +157 -67
  85. package/src/components/resources/ResourcesView.tsx +44 -6
  86. package/src/components/resources/renderers/NamespaceRenderer.tsx +7 -1
  87. package/src/components/resources/renderers/PodRenderer.tsx +4 -0
  88. package/src/components/resources/renderers/RolloutRenderer.tsx +15 -1
  89. package/src/components/resources/renderers/WorkloadRenderer.tsx +3 -0
  90. package/src/components/rightsizing/RightsizingScanView.tsx +21 -0
  91. package/src/components/settings/SettingsDialog.tsx +94 -14
  92. package/src/components/traffic/TrafficFlowList.tsx +229 -70
  93. package/src/components/traffic/TrafficGraph.tsx +6 -6
  94. package/src/components/traffic/TrafficView.tsx +10 -9
  95. package/src/components/traffic/trafficFilters.test.ts +28 -1
  96. package/src/components/traffic/trafficFilters.ts +31 -0
  97. package/src/components/workload/WorkloadView.tsx +19 -8
  98. package/src/context/ConnectionContext.tsx +48 -3
  99. package/src/context/DiagnoseCustomization.test.tsx +26 -0
  100. package/src/context/DiagnoseCustomization.tsx +15 -2
  101. package/src/index.css +17 -0
  102. package/src/index.ts +1 -0
  103. package/src/utils/navigation.ts +1 -1
  104. package/src/utils/shell-safe.test.ts +25 -1
  105. package/src/utils/shell-safe.ts +16 -0
  106. package/src/utils/topology-namespace.test.ts +64 -0
  107. package/src/utils/topology-namespace.ts +18 -0
  108. package/src/components/diagnose/investigationEvidence.test.ts +0 -5202
  109. package/src/components/diagnose/investigationEvidence.ts +0 -4784
  110. package/src/components/gitops/GitOpsActions.tsx +0 -1
  111. package/src/components/gitops/GitOpsStatusBadge.tsx +0 -1
  112. package/src/components/gitops/ManagedResourcesList.tsx +0 -1
  113. package/src/components/gitops/SyncCountdown.tsx +0 -1
  114. package/src/components/gitops/index.ts +0 -4
  115. package/src/components/helm/ManifestDiffViewer.tsx +0 -69
  116. package/src/components/logs/JsonLogLine.tsx +0 -1
  117. package/src/components/logs/LogCore.tsx +0 -2
  118. package/src/components/logs/useLogBuffer.ts +0 -2
  119. package/src/components/logs/useLogSearch.ts +0 -1
  120. package/src/components/resource-drawer/ResourceDrawer.tsx +0 -216
  121. package/src/components/resources/drawer-components.tsx +0 -1
  122. package/src/components/resources/renderers/AlertRenderer.tsx +0 -1
  123. package/src/components/resources/renderers/CNPGBackupRenderer.tsx +0 -1
  124. package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +0 -1
  125. package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +0 -1
  126. package/src/components/resources/renderers/CertificateRenderer.tsx +0 -1
  127. package/src/components/resources/renderers/CertificateRequestRenderer.tsx +0 -1
  128. package/src/components/resources/renderers/ChallengeRenderer.tsx +0 -1
  129. package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +0 -1
  130. package/src/components/resources/renderers/ClusterExternalSecretRenderer.tsx +0 -1
  131. package/src/components/resources/renderers/ClusterIssuerRenderer.tsx +0 -1
  132. package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +0 -1
  133. package/src/components/resources/renderers/ConfigMapRenderer.tsx +0 -1
  134. package/src/components/resources/renderers/CronJobRenderer.tsx +0 -1
  135. package/src/components/resources/renderers/CronWorkflowRenderer.tsx +0 -1
  136. package/src/components/resources/renderers/EventRenderer.tsx +0 -1
  137. package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +0 -1
  138. package/src/components/resources/renderers/ExternalSecretRenderer.tsx +0 -1
  139. package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +0 -1
  140. package/src/components/resources/renderers/GRPCRouteRenderer.tsx +0 -1
  141. package/src/components/resources/renderers/GatewayClassRenderer.tsx +0 -1
  142. package/src/components/resources/renderers/GatewayRenderer.tsx +0 -1
  143. package/src/components/resources/renderers/GenericRenderer.tsx +0 -1
  144. package/src/components/resources/renderers/GitRepositoryRenderer.tsx +0 -1
  145. package/src/components/resources/renderers/HTTPRouteRenderer.tsx +0 -1
  146. package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +0 -1
  147. package/src/components/resources/renderers/IngressClassRenderer.tsx +0 -1
  148. package/src/components/resources/renderers/IngressRenderer.tsx +0 -1
  149. package/src/components/resources/renderers/IstioAuthorizationPolicyRenderer.tsx +0 -1
  150. package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +0 -1
  151. package/src/components/resources/renderers/IstioGatewayRenderer.tsx +0 -1
  152. package/src/components/resources/renderers/IstioPeerAuthenticationRenderer.tsx +0 -1
  153. package/src/components/resources/renderers/IstioServiceEntryRenderer.tsx +0 -1
  154. package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +0 -1
  155. package/src/components/resources/renderers/JobRenderer.tsx +0 -1
  156. package/src/components/resources/renderers/KarpenterEC2NodeClassRenderer.tsx +0 -1
  157. package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +0 -1
  158. package/src/components/resources/renderers/KedaScaledJobRenderer.tsx +0 -1
  159. package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +0 -1
  160. package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +0 -1
  161. package/src/components/resources/renderers/KnativeConfigurationRenderer.tsx +0 -1
  162. package/src/components/resources/renderers/KnativeEventingRenderer.tsx +0 -1
  163. package/src/components/resources/renderers/KnativeFlowRenderer.tsx +0 -1
  164. package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +0 -1
  165. package/src/components/resources/renderers/KnativeRevisionRenderer.tsx +0 -1
  166. package/src/components/resources/renderers/KnativeRouteRenderer.tsx +0 -1
  167. package/src/components/resources/renderers/KnativeServiceRenderer.tsx +0 -1
  168. package/src/components/resources/renderers/KnativeSourceRenderer.tsx +0 -1
  169. package/src/components/resources/renderers/KustomizationRenderer.tsx +0 -1
  170. package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +0 -1
  171. package/src/components/resources/renderers/LeaseRenderer.tsx +0 -1
  172. package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +0 -1
  173. package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +0 -1
  174. package/src/components/resources/renderers/OrderRenderer.tsx +0 -1
  175. package/src/components/resources/renderers/PersistentVolumeRenderer.tsx +0 -1
  176. package/src/components/resources/renderers/PodDisruptionBudgetRenderer.tsx +0 -1
  177. package/src/components/resources/renderers/PodMonitorRenderer.tsx +0 -1
  178. package/src/components/resources/renderers/PriorityClassRenderer.tsx +0 -1
  179. package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +0 -1
  180. package/src/components/resources/renderers/ReplicaSetRenderer.tsx +0 -1
  181. package/src/components/resources/renderers/RuntimeClassRenderer.tsx +0 -1
  182. package/src/components/resources/renderers/SbomReportRenderer.tsx +0 -1
  183. package/src/components/resources/renderers/SealedSecretRenderer.tsx +0 -1
  184. package/src/components/resources/renderers/SecretRenderer.tsx +0 -1
  185. package/src/components/resources/renderers/SecretStoreRenderer.tsx +0 -1
  186. package/src/components/resources/renderers/ServiceMonitorRenderer.tsx +0 -1
  187. package/src/components/resources/renderers/SimpleRouteRenderer.tsx +0 -1
  188. package/src/components/resources/renderers/StorageClassRenderer.tsx +0 -1
  189. package/src/components/resources/renderers/TraefikIngressRouteRenderer.tsx +0 -1
  190. package/src/components/resources/renderers/VPARenderer.tsx +0 -1
  191. package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +0 -1
  192. package/src/components/resources/renderers/VeleroVSLRenderer.tsx +0 -1
  193. package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +0 -1
  194. package/src/components/resources/renderers/WebhookConfigRenderer.tsx +0 -1
  195. package/src/components/resources/renderers/WorkflowRenderer.tsx +0 -1
  196. package/src/components/resources/renderers/WorkflowTemplateRenderer.tsx +0 -1
  197. package/src/components/resources/renderers/argo-cells.tsx +0 -1
  198. package/src/components/resources/renderers/certmanager-cells.tsx +0 -1
  199. package/src/components/resources/renderers/cnpg-cells.tsx +0 -1
  200. package/src/components/resources/renderers/eso-cells.tsx +0 -1
  201. package/src/components/resources/renderers/flux-cells.tsx +0 -1
  202. package/src/components/resources/renderers/index.ts +0 -94
  203. package/src/components/resources/renderers/istio-cells.tsx +0 -1
  204. package/src/components/resources/renderers/karpenter-cells.tsx +0 -1
  205. package/src/components/resources/renderers/keda-cells.tsx +0 -1
  206. package/src/components/resources/renderers/knative-cells.tsx +0 -1
  207. package/src/components/resources/renderers/kyverno-cells.tsx +0 -1
  208. package/src/components/resources/renderers/prometheus-cells.tsx +0 -1
  209. package/src/components/resources/renderers/traefik-cells.tsx +0 -1
  210. package/src/components/resources/renderers/trivy-cells.tsx +0 -1
  211. package/src/components/resources/renderers/trivy-shared.tsx +0 -1
  212. package/src/components/resources/renderers/velero-cells.tsx +0 -1
  213. package/src/components/resources/resource-utils-argo.ts +0 -2
  214. package/src/components/resources/resource-utils-certmanager.ts +0 -2
  215. package/src/components/resources/resource-utils-eso.ts +0 -2
  216. package/src/components/resources/resource-utils-flux.ts +0 -2
  217. package/src/components/resources/resource-utils-istio.ts +0 -2
  218. package/src/components/resources/resource-utils-karpenter.ts +0 -2
  219. package/src/components/resources/resource-utils-knative.ts +0 -2
  220. package/src/components/resources/resource-utils-kyverno.ts +0 -2
  221. package/src/components/resources/resource-utils-prometheus.ts +0 -2
  222. package/src/components/resources/resource-utils-traefik.ts +0 -1
  223. package/src/components/resources/resource-utils-trivy.ts +0 -2
  224. package/src/components/resources/resource-utils-velero.ts +0 -2
  225. package/src/components/shared/EditableYamlView.tsx +0 -24
  226. package/src/components/timeline/DiffViewer.tsx +0 -1
  227. package/src/components/traffic/index.ts +0 -3
  228. package/src/components/ui/ForceDeleteConfirmDialog.tsx +0 -1
  229. package/src/components/ui/MetricsChart.tsx +0 -1
  230. package/src/components/ui/ResourceBar.tsx +0 -1
@@ -0,0 +1,614 @@
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, prior-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
+ for (const evidence of [
497
+ { status: "linked" as const, refs: [evidenceRef("a", "z")] },
498
+ { status: "linked" as const, refs: [priorRef] },
499
+ { status: "linked" as const, refs: [failedRef] },
500
+ { status: "linked" as const, refs: [partialRef] },
501
+ { status: "linked" as const, refs: [currentRef, currentRef] },
502
+ { status: "linked" as const, refs: [] },
503
+ { status: "linked" as const, refs: ["invalid"] },
504
+ ]) {
505
+ expect(
506
+ resolveInvestigationRootCauseEvidence(projection, evidence, 1),
507
+ ).toEqual({ status: "invalid", links: [] });
508
+ }
509
+ expect(
510
+ resolveInvestigationRootCauseEvidence(
511
+ projection,
512
+ { status: "linked", refs: [currentRef, evidenceRef("a", "z")] },
513
+ 1,
514
+ ),
515
+ ).toEqual({ status: "invalid", links: [] });
516
+
517
+ const duplicateSourceProjection = project([
518
+ tool("first", "get_resource", deployment, {
519
+ evidenceRef: currentRef,
520
+ }),
521
+ tool("second", "get_resource", deployment, {
522
+ evidenceRef: currentRef,
523
+ }),
524
+ ]);
525
+ expect(
526
+ resolveInvestigationRootCauseEvidence(
527
+ duplicateSourceProjection,
528
+ { status: "linked", refs: [currentRef] },
529
+ 0,
530
+ ),
531
+ ).toEqual({ status: "invalid", links: [] });
532
+ });
533
+
534
+ it("counts ineligible duplicate refs only in the assessment turn", () => {
535
+ const ref = evidenceRef("a", "b");
536
+ const currentDuplicate = project(
537
+ [tool("prior", "get_resource", deployment, { evidenceRef: ref })],
538
+ [
539
+ tool("current", "get_resource", deployment, { evidenceRef: ref }),
540
+ tool("failed-duplicate", "get_resource", deployment, {
541
+ evidenceRef: ref,
542
+ isError: true,
543
+ }),
544
+ ],
545
+ );
546
+ expect(currentDuplicate.evidenceRefSources).toHaveLength(3);
547
+ expect(
548
+ resolveInvestigationRootCauseEvidence(
549
+ currentDuplicate,
550
+ { status: "linked", refs: [ref] },
551
+ 1,
552
+ ),
553
+ ).toEqual({ status: "invalid", links: [] });
554
+
555
+ const priorDuplicateOnly = project(
556
+ [
557
+ tool("prior-first", "get_resource", deployment, { evidenceRef: ref }),
558
+ tool("prior-second", "get_resource", deployment, {
559
+ evidenceRef: ref,
560
+ isError: true,
561
+ }),
562
+ ],
563
+ [tool("current-only", "get_resource", deployment, { evidenceRef: ref })],
564
+ );
565
+ expect(
566
+ resolveInvestigationRootCauseEvidence(
567
+ priorDuplicateOnly,
568
+ { status: "linked", refs: [ref] },
569
+ 1,
570
+ ),
571
+ ).toMatchObject({
572
+ status: "linked",
573
+ links: [{ source: { stepId: "current-only" } }],
574
+ });
575
+ });
576
+
577
+ it("rejects a replay that claims refs from different investigation scopes are linked", () => {
578
+ const firstRef = evidenceRef("a", "b");
579
+ const secondRef = evidenceRef("c", "d");
580
+ const projection = project([
581
+ tool("first", "get_resource", deployment, { evidenceRef: firstRef }),
582
+ tool("second", "get_resource", deployment, { evidenceRef: secondRef }),
583
+ ]);
584
+
585
+ expect(
586
+ resolveInvestigationRootCauseEvidence(
587
+ projection,
588
+ { status: "linked", refs: [firstRef, secondRef] },
589
+ 0,
590
+ ),
591
+ ).toEqual({ status: "invalid", links: [] });
592
+ });
593
+
594
+ it("preserves explicit missing and invalid server states without inventing links", () => {
595
+ const projection = project([]);
596
+ expect(
597
+ resolveInvestigationRootCauseEvidence(projection, undefined, 0),
598
+ ).toEqual({ status: "missing", links: [] });
599
+ expect(
600
+ resolveInvestigationRootCauseEvidence(
601
+ projection,
602
+ { status: "missing" },
603
+ 0,
604
+ ),
605
+ ).toEqual({ status: "missing", links: [] });
606
+ expect(
607
+ resolveInvestigationRootCauseEvidence(
608
+ projection,
609
+ { status: "invalid" },
610
+ 0,
611
+ ),
612
+ ).toEqual({ status: "invalid", links: [] });
613
+ });
614
+ });