@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,351 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
2
+ import { renderToString } from 'react-dom/server'
3
+ import { MemoryRouter } from 'react-router-dom'
4
+ import type { WorkloadMetrics, WorkloadMetricPanel } from '../../api/workloadMetrics'
5
+
6
+ let result: { data?: WorkloadMetrics; isLoading?: boolean; isPlaceholderData?: boolean; error?: Error } = {}
7
+ vi.mock('../../api/workloadMetrics', () => ({ useWorkloadMetrics: () => result }))
8
+ const { WorkloadMetricsSection } = await import('./WorkloadMetricsSection')
9
+ const panel = (value: number, unit = 'requests/s'): WorkloadMetricPanel => ({
10
+ state: 'available', unit,
11
+ series: [{ labels: { pod: 'checkout-0' }, dataPoints: [{ timestamp: 100, value }, { timestamp: 160, value }] }],
12
+ })
13
+ const render = () => renderToString(<MemoryRouter><WorkloadMetricsSection kind="Deployment" namespace="shop" name="checkout" range="1h" /></MemoryRouter>)
14
+
15
+ beforeEach(() => {
16
+ result = { data: {
17
+ state: 'available', source: 'beyla',
18
+ sources: [{ id: 'beyla', label: 'Beyla', state: 'available' }],
19
+ pods: 2, podsTotal: 2, start: 100, end: 160, stepSeconds: 60, rateWindowSeconds: 300,
20
+ history: { cpu: { mode: 'current-pods', reason: 'Missing retained ownership.' }, requests: { mode: 'current-pods' } },
21
+ comparison: { cpu: panel(0, 'cores') },
22
+ panels: { requests: panel(0), observedPods: panel(1, 'pods'), cpu: panel(0, 'cores') },
23
+ } }
24
+ })
25
+
26
+ describe('Workload metrics interpretation', () => {
27
+ it('shows matching evidence instead of claiming no observations when no observer is usable', () => {
28
+ result.data!.source = undefined
29
+ result.data!.sources = [{ id: 'beyla', label: 'Beyla', state: 'unavailable' }]
30
+ result.data!.attribution = { beyla: 'Metrics exist, but their cluster identity could not be established automatically.' }
31
+ result.data!.panels.requests = { state: 'unavailable', unit: 'requests/s', series: [], reason: 'Generic prerequisites' }
32
+ const html = render()
33
+ expect(html).toContain('No usable HTTP metrics in this window')
34
+ expect(html).toContain('Metrics exist, but their cluster identity')
35
+ expect(html).not.toContain('Generic prerequisites')
36
+ expect(html).not.toContain('No HTTP observations')
37
+ })
38
+ it('preserves a selected observer’s query reason rather than replacing it with matching evidence', () => {
39
+ result.data!.attribution = { beyla: 'Matched to current Pod UIDs' }
40
+ result.data!.panels.requests = { state: 'unavailable', unit: 'requests/s', series: [], reason: 'No samples from the selected observer' }
41
+ const html = render()
42
+ expect(html).toContain('No samples from the selected observer')
43
+ expect(html).not.toContain('Matched to current Pod UIDs')
44
+ })
45
+ it('separates historical aggregates from bounded current comparison', () => {
46
+ result.data!.podsTotal = 128
47
+ result.data!.pods = 100
48
+ result.data!.history = { cpu: { mode: 'workload-history' }, requests: { mode: 'workload-history' } }
49
+ result.data!.panels.cpu = { ...panel(128, 'cores'), series: [
50
+ { labels: { aggregation: 'Workload' }, dataPoints: [{ timestamp: 160, value: 128 }] },
51
+ { labels: { aggregation: 'Maximum Pod' }, dataPoints: [{ timestamp: 160, value: 1 }] },
52
+ ] }
53
+ const html = render().replaceAll('<!-- -->', '')
54
+ expect(html).toContain('CPU usage · workload')
55
+ expect(html).toContain('Maximum Pod')
56
+ expect(html).toContain('1 Pods reporting')
57
+ expect(html).not.toContain('of 128 current Pods reporting')
58
+ expect(html).toContain('checkout-0')
59
+ expect(html).toContain('Comparing 100 of 128 current Pods')
60
+ expect(html).toContain('when ownership disappears after Pod deletion')
61
+ })
62
+ it('names the current-only fallback rather than implying full history', () => {
63
+ const html = render()
64
+ expect(html).toContain('Current Pods only')
65
+ expect(html).toContain('Missing retained ownership')
66
+ })
67
+ it('keeps name-matched fallback charts separate from identity-checked metrics', () => {
68
+ result.data!.panels.cpu = { state: 'error', unit: 'cores', reason: 'History lookup failed', series: [] }
69
+ result.data!.panels.memory = panel(100, 'bytes')
70
+ const html = renderToString(<MemoryRouter><WorkloadMetricsSection kind="Deployment" namespace="shop" name="checkout" range="1h" nameMatchedCharts={{ cpu: <span>Existing CPU chart</span>, memory: <span>Existing memory chart</span> }} /></MemoryRouter>)
71
+ expect(html.replaceAll('<!-- -->', '')).toContain('Basic CPU · identity unverified')
72
+ expect(html).toContain('<details aria-label="Name-matched resource metrics">')
73
+ expect(html).toContain('matching names in a shared backend may include another cluster')
74
+ expect(html).toContain('Existing CPU chart')
75
+ expect(html).toContain('History lookup failed')
76
+ expect(html).not.toContain('Existing memory chart')
77
+ })
78
+ it('does not flash name-matched charts during the initial request', () => {
79
+ result = { isLoading: true }
80
+ const html = renderToString(<MemoryRouter><WorkloadMetricsSection kind="Deployment" namespace="shop" name="checkout" range="1h" nameMatchedCharts={{ cpu: <span>Existing CPU chart</span> }} /></MemoryRouter>)
81
+ expect(html).not.toContain('Existing CPU chart')
82
+ expect(html).toContain('Checking request metrics')
83
+ })
84
+ it('shows only supplied template values and qualifies actual Pod differences', () => {
85
+ const html = renderToString(<MemoryRouter><WorkloadMetricsSection kind="Deployment" namespace="shop" name="checkout" range="1h" cpuReferenceLines={[{ value: 0.1, label: 'request 100m', kind: 'request' }]} memoryReferenceLines={[]} /></MemoryRouter>).replaceAll('<!-- -->', '')
86
+ expect(html).toContain('Template per Pod · actual Pods may differ')
87
+ expect(html).toContain('Template request 100m')
88
+ expect(html).toContain('Actual Pods can differ after injection or rollout.')
89
+ expect(html).not.toContain(' · Memory:')
90
+ expect(html).not.toContain('unlimited')
91
+ expect(render()).not.toContain('Template per Pod')
92
+ })
93
+ it.each([true, false])('keeps absent HTTP compact without narrating resource availability: %s', (usable) => {
94
+ result.data!.panels.requests = { state: 'unavailable', unit: 'requests/s', series: [], reason: 'No observations' }
95
+ result.data!.panels.cpu = usable ? panel(0, 'cores') : { state: 'error', unit: 'cores', series: [], reason: 'Query failed' }
96
+ const html = render()
97
+ expect(html).toContain('No usable HTTP metrics in this window')
98
+ expect(html).not.toContain('Resource charts below remain available')
99
+ expect(html).not.toContain('--beyla-job-selector')
100
+ if (!usable) expect(html).not.toContain('CPU usage · per Pod')
101
+ })
102
+ it('does not call pending attribution a lack of HTTP traffic', () => {
103
+ result.data!.panels.requests = { state: 'detecting', unit: 'requests/s', series: [], reason: 'Matching identity' }
104
+ const html = render()
105
+ expect(html).toContain('Checking request metrics…')
106
+ expect(html).toContain('Matching identity')
107
+ expect(html).not.toContain('No usable HTTP metrics')
108
+ })
109
+ it('keeps empty resource titles neutral even with an all-null series', () => {
110
+ result.data!.panels.cpu!.series[0].dataPoints = [{ timestamp: 160, value: null }]
111
+ const html = render()
112
+ expect(html).toContain('CPU usage')
113
+ expect(html).not.toContain('CPU usage · per Pod')
114
+ })
115
+ it('discloses combined request ports and reporting sidecars', () => {
116
+ const html = render()
117
+ expect(html).toContain('Ports combined · includes health checks and admin traffic.')
118
+ expect(html).toContain('Resource totals include reporting containers and sidecars.')
119
+ })
120
+ it('describes throttling scope independently of a failed CPU history query', () => {
121
+ result.data!.history.cpu = { mode: 'unavailable' }
122
+ result.data!.history.throttling = { mode: 'workload-history' }
123
+ result.data!.panels.throttling = panel(5, 'percent')
124
+ const html = render().replaceAll('<!-- -->', '')
125
+ expect(html).toContain('Throttling is weighted by total periods')
126
+ expect(html).not.toContain('Examines 2 of 2 current Pods')
127
+ })
128
+ it('does not describe aggregate series in current-Pod help', () => {
129
+ result.data!.history.throttling = { mode: 'current-pods' }
130
+ result.data!.panels.throttling = panel(5, 'percent')
131
+ const html = render()
132
+ expect(html).not.toContain('Workload is the total across reporting Pods')
133
+ expect(html).not.toContain('Throttling is weighted by total periods')
134
+ })
135
+ it('explains a historical memory aggregate without implying historical throttling', () => {
136
+ result.data!.history.memory = { mode: 'workload-history' }
137
+ result.data!.panels.memory = panel(100, 'bytes')
138
+ result.data!.history.throttling = { mode: 'current-pods' }
139
+ result.data!.panels.throttling = panel(5, 'percent')
140
+ const html = render()
141
+ expect(html).toContain('In workload-history CPU and memory charts, Workload is the total')
142
+ expect(html).not.toContain('Throttling is weighted by total periods')
143
+ })
144
+ it('retains resource charts while loading another request observer', () => {
145
+ result.isPlaceholderData = true
146
+ const html = render()
147
+ expect(html).toContain('Loading selected request source')
148
+ expect(html).toContain('CPU usage · per Pod')
149
+ expect(html).toContain('Compare current Pods')
150
+ expect(html).not.toContain('1 of 2 current Pods reporting')
151
+ })
152
+ it('labels operator scope and discarded assertions visibly', () => {
153
+ result.data!.attribution = { scope: 'Operator-asserted cluster scope' }
154
+ result.data!.scopeNotice = 'Scope assertion discarded after reconnect'
155
+ const html = render()
156
+ expect(html).toContain('operator-asserted scope')
157
+ expect(html).toContain('Scope assertion discarded after reconnect')
158
+ expect(html).not.toContain('Automatic metrics attribution')
159
+ })
160
+ it('explains withheld populations instead of suggesting instrumentation is absent', () => {
161
+ result.data!.panels.requests = {state:'partial',unit:'requests/s',series:[],reason:'Multiple observation populations'}
162
+ const html = render()
163
+ expect(html).toContain('Request metrics withheld')
164
+ expect(html).not.toContain('No request observations')
165
+ expect(html).not.toContain('--beyla-job-selector')
166
+ })
167
+ it.each(['detecting', 'unavailable', 'error'] as const)('does not substitute unattributed resource charts while metrics are %s', (state) => {
168
+ result.data!.state = state
169
+ result.data!.panels = {}
170
+ const html = render()
171
+ expect(html).not.toContain('CPU usage · per Pod')
172
+ expect(html).not.toContain('Memory working set · per Pod')
173
+ })
174
+ it('groups CPU and memory with throttling before the full-width Pod comparison', () => {
175
+ result.data!.panels.throttling = panel(5, 'percent')
176
+ const html = render()
177
+ expect(html.indexOf('CPU usage · per Pod')).toBeLessThan(html.indexOf('CPU throttled periods'))
178
+ expect(html.indexOf('CPU throttled periods')).toBeLessThan(html.indexOf('Compare current Pods'))
179
+ expect(html).toContain('metrics-chart-wide')
180
+ expect(html).not.toContain('lg:grid-cols-3')
181
+ })
182
+ it('distinguishes reporting Pods from selected Pods and preserves measured zero', () => {
183
+ const html = render()
184
+ expect(html).toContain('1 of 2 current Pods reporting')
185
+ expect(html).toContain('tabular-nums text-theme-text-primary">0</span>')
186
+ expect(html).toContain('previous replicas are not reconstructed')
187
+ expect(html).toContain('/resources/pods?resource=shop%2Fcheckout-0')
188
+ expect(html).toContain('checkout-0')
189
+ })
190
+ it('does not substitute selected count when coverage is missing', () => {
191
+ delete result.data!.panels.observedPods
192
+ expect(render()).toContain('Reporting Pod count unavailable')
193
+ })
194
+ it('keeps operator overrides out of the chart surface', () => {
195
+ result.data!.attribution = { cpu: 'Metrics exist, but their cluster identity could not be established automatically.' }
196
+ const html = render()
197
+ expect(html).toContain('aria-haspopup="dialog"')
198
+ expect(html).not.toContain('--prometheus-single-cluster')
199
+ expect(html).toContain('Workload resource pressure')
200
+ expect(html).not.toContain('operator setup required')
201
+ })
202
+ it('shows detecting without claiming missing data or mandatory setup', () => {
203
+ result.data!.state = 'detecting'
204
+ result.data!.reason = 'Matching metrics to current Pods…'
205
+ result.data!.panels = {}
206
+ const html = render()
207
+ expect(html).toContain('Matching metrics to current Pods')
208
+ expect(html).not.toContain('No request observations')
209
+ expect(html).not.toContain('--prometheus-single-cluster')
210
+ })
211
+ it('discloses partial pressure coverage', () => {
212
+ result.data!.panels.cpu = { ...panel(1, 'cores'), state: 'partial', reason: 'Attributed to 1 of 2 current Pods; other Pods are excluded.' }
213
+ expect(render()).toContain('Attributed to 1 of 2 current Pods')
214
+ })
215
+ it('retains response-level population limitations outside help', () => {
216
+ result.data!.state = 'partial'
217
+ result.data!.reason = 'Current pod population was capped; values cover only the included pods.'
218
+ expect(render()).toContain('Current pod population was capped; values cover only the included pods.')
219
+ })
220
+ it('does not show setup instructions for a workload with no Pods', () => {
221
+ result.data!.state = 'unavailable'
222
+ result.data!.reason = 'No current Pods.'
223
+ expect(render()).not.toContain('operator setup required')
224
+ })
225
+ it('shows query failure rather than fabricating missing metrics', () => {
226
+ result = { error: new Error('Forbidden') }
227
+ expect(render().replaceAll('<!-- -->', '')).toContain('Workload metrics could not be loaded: Forbidden')
228
+ })
229
+ it('keeps observers explicitly separate when both are available', () => {
230
+ result.data!.sources.push({ id: 'istio', label: 'Istio', state: 'available' })
231
+ expect(render()).toContain('Request metrics source')
232
+ expect(render()).toContain('value="beyla" selected=""')
233
+ })
234
+ it('does not suggest a job matcher for a failed request query', () => {
235
+ result.data!.panels.requests = { state: 'error', unit: 'requests/s', series: [], reason: 'Query failed' }
236
+ const html = render()
237
+ expect(html).toContain('Request metrics query failed')
238
+ expect(html).not.toContain('--beyla-job-selector')
239
+ })
240
+ it('describes Istio at the selected destination-sidecar observer', () => {
241
+ result.data!.source = 'istio'
242
+ result.data!.sources = [{ id: 'istio', label: 'Istio · destination sidecars', state: 'available' }]
243
+ const html = render().replaceAll('<!-- -->', '')
244
+ expect(html.match(/Istio · destination sidecars/g)).toHaveLength(1)
245
+ expect(html).toContain('5-minute rates')
246
+ expect(html).not.toContain('HTTP server ·')
247
+ })
248
+ it('does not describe history for an empty Requests section', () => {
249
+ result.data!.panels.requests = { state: 'unavailable', unit: 'requests/s', series: [] }
250
+ result.data!.history.requests = { mode: 'workload-history', reason: 'Request-only history marker' }
251
+ const html = render()
252
+ expect(html).not.toContain('Request-only history marker')
253
+ expect(html).toContain('No usable HTTP metrics in this window')
254
+ })
255
+ it('keeps successful attribution evidence and definitions collapsed', () => {
256
+ result.data!.attribution = { cpu: 'Matched CPU identity' }
257
+ const html = render()
258
+ expect(html).toContain('About these metrics')
259
+ expect(html).not.toContain('Matched CPU identity')
260
+ expect(html).toContain('Resource totals include reporting containers and sidecars.')
261
+ expect(html).not.toMatch(/<details[^>]*\bopen[=> ]/)
262
+ })
263
+ it('deduplicates stale RED notices without losing individual reasons', () => {
264
+ for (const key of ['requests', 'errors', 'p50', 'p95'] as const) {
265
+ result.data!.panels[key] = { ...panel(1), state: 'stale', reason: `${key} history only` }
266
+ }
267
+ const html = render()
268
+ expect(html.match(/no recent samples/g)).toHaveLength(1)
269
+ for (const key of ['requests', 'errors', 'p50', 'p95']) expect(html).toContain(`${key} history only`)
270
+ })
271
+ it('shares identical partial request coverage while keeping the warning visible', () => {
272
+ for (const key of ['requests', 'errors', 'p50', 'p95'] as const) {
273
+ result.data!.panels[key] = { ...panel(1), state: 'partial', reason: 'Only one reporting Pod was identified.' }
274
+ }
275
+ const html = render().replaceAll('<!-- -->', '')
276
+ expect(html.match(/Only one reporting Pod was identified/g)).toHaveLength(1)
277
+ expect(html).toContain('Requests, HTTP 5xx and latency: Only one reporting Pod was identified.')
278
+ })
279
+ it.each([true, false])('shares partial resource coverage only with matching scopes: %s', (sameScope) => {
280
+ for (const key of ['cpu', 'memory', 'throttling'] as const) {
281
+ result.data!.history[key] = { mode: 'current-pods' }
282
+ result.data!.panels[key] = { ...panel(1), state: 'partial', reason: 'Attributed to 1 of 2 Pods.' }
283
+ }
284
+ if (!sameScope) result.data!.history.memory = { mode: 'workload-history' }
285
+ expect(render().match(/Attributed to 1 of 2 Pods/g)).toHaveLength(sameScope ? 1 : 3)
286
+ })
287
+ it('does not hide a withheld chart behind a shared partial warning', () => {
288
+ for (const key of ['requests', 'errors', 'p50', 'p95'] as const) {
289
+ result.data!.panels[key] = { ...panel(1), state: 'partial', reason: 'Incomplete observation population.' }
290
+ }
291
+ result.data!.panels.p95!.series = []
292
+ result.data!.panels.p50!.series = []
293
+ const html = render()
294
+ expect(html).not.toContain('Requests, HTTP 5xx and latency:')
295
+ expect(html.match(/Incomplete observation population/g)).toHaveLength(3)
296
+ })
297
+ it('keeps stale samples withheld by coverage checks local to their chart', () => {
298
+ for (const key of ['requests', 'errors', 'p50', 'p95'] as const) {
299
+ result.data!.panels[key] = { ...panel(1), state: 'stale', reason: 'Historical samples only' }
300
+ }
301
+ result.data!.panels.errors!.series[0].dataPoints = [{ timestamp: 100, value: null }]
302
+ result.data!.panels.errors!.reason = 'Historical samples only. HTTP status coverage is incomplete.'
303
+ const html = render()
304
+ expect(html).not.toContain('Requests, HTTP 5xx and latency:')
305
+ expect(html).toContain('HTTP status coverage is incomplete')
306
+ expect(html).toMatch(/<p role="status"[^>]*>Historical samples only\. HTTP status coverage is incomplete\.<\/p>/)
307
+ })
308
+ it('keeps stale and failed quantiles local when RED states differ', () => {
309
+ result.data!.panels.p95 = { ...panel(1, 'seconds'), state: 'stale', reason: 'Old latency' }
310
+ result.data!.panels.p50 = { state: 'error', unit: 'seconds', series: [], reason: 'Median query failed' }
311
+ const html = render()
312
+ expect(html).toContain('Historical samples only')
313
+ expect(html).toContain('Median query failed')
314
+ expect(html).not.toContain('Requests, HTTP 5xx and latency:')
315
+ })
316
+ it('shows an identical empty-quantile explanation once', () => {
317
+ result.data!.panels.p50 = { state: 'unavailable', unit: 'seconds', series: [], reason: 'No histogram samples' }
318
+ result.data!.panels.p95 = { state: 'unavailable', unit: 'seconds', series: [], reason: 'No histogram samples' }
319
+ expect(render().match(/No histogram samples/g)).toHaveLength(1)
320
+ })
321
+ it('does not repeat a chart error in its history details', () => {
322
+ result.data!.history.cpu = { mode: 'unavailable', reason: 'CPU history query failed' }
323
+ result.data!.panels.cpu = { state: 'error', unit: 'cores', series: [], reason: 'CPU history query failed' }
324
+ expect(render().match(/CPU history query failed/g)).toHaveLength(1)
325
+ })
326
+ it('shows comparison matching once, not as missing Pod samples', () => {
327
+ result.data!.comparison = {
328
+ cpu: { state: 'detecting', unit: 'cores', series: [], reason: 'Matching metrics' },
329
+ memory: { state: 'detecting', unit: 'bytes', series: [], reason: 'Matching metrics' },
330
+ }
331
+ const html = render().replaceAll('<!-- -->', '')
332
+ expect(html.match(/Matching CPU \/ Memory metrics to current Pods/g)).toHaveLength(1)
333
+ expect(html).not.toContain('No Pod samples available')
334
+ })
335
+ it('deduplicates identical comparison failures and retains affected metrics', () => {
336
+ result.data!.comparison = {
337
+ cpu: { state: 'error', unit: 'cores', series: [], reason: 'Comparison query failed' },
338
+ memory: { state: 'error', unit: 'bytes', series: [], reason: 'Comparison query failed' },
339
+ }
340
+ const html = render().replaceAll('<!-- -->', '')
341
+ expect(html.match(/Comparison query failed/g)).toHaveLength(1)
342
+ expect(html).toContain('CPU / Memory: Comparison query failed')
343
+ expect(html).not.toContain('No Pod samples available')
344
+ })
345
+ it('does not explain an empty resource chart in terms of HTTP traffic', () => {
346
+ result.data!.panels.cpu = { state: 'unavailable', unit: 'cores', series: [] }
347
+ const html = render()
348
+ expect(html).toContain('No usable samples in this window')
349
+ expect(html).not.toContain('Idle traffic has no defined error percentage or latency')
350
+ })
351
+ })