@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
@@ -9,6 +9,11 @@ import {
9
9
  TurnView,
10
10
  appendThinking,
11
11
  remediationCommands,
12
+ mergeStartupSignal,
13
+ middleTruncate,
14
+ runningElapsedLabel,
15
+ toolDurationLabel,
16
+ toolErrorReason,
12
17
  upsertTool,
13
18
  type TimelineItem,
14
19
  type Turn,
@@ -17,6 +22,7 @@ import type {
17
22
  AgentInfo,
18
23
  Diagnosis,
19
24
  DiagnoseStep,
25
+ DiagnoseStreamEvent,
20
26
  ExecutionProfile,
21
27
  } from "../../api/diagnose";
22
28
  import { investigationEvidenceSourceId } from "./investigationEvidence";
@@ -377,6 +383,385 @@ describe("Timeline reasoning density", () => {
377
383
  });
378
384
  });
379
385
 
386
+ describe("Timeline startup signals", () => {
387
+ const readyEvent: Pick<
388
+ DiagnoseStreamEvent,
389
+ "phase" | "model" | "toolCount" | "mcpServers"
390
+ > = {
391
+ phase: "ready",
392
+ model: "claude-opus-5",
393
+ toolCount: 24,
394
+ mcpServers: [{ name: "radar", status: "connected" }],
395
+ };
396
+
397
+ it("reports each startup phase in place of the generic pending label", () => {
398
+ const before = renderToStaticMarkup(
399
+ <Timeline items={[]} running agentLabel="Claude Code" />,
400
+ );
401
+ expect(before).toContain("Starting investigation…");
402
+
403
+ const starting = mergeStartupSignal(undefined, { phase: "investigating" });
404
+ expect(
405
+ renderToStaticMarkup(
406
+ <Timeline
407
+ items={[]}
408
+ running
409
+ agentLabel="Claude Code"
410
+ startup={starting}
411
+ />,
412
+ ),
413
+ ).toContain("Claude Code starting…");
414
+
415
+ const connected = mergeStartupSignal(starting, { phase: "connected" });
416
+ expect(
417
+ renderToStaticMarkup(
418
+ <Timeline
419
+ items={[]}
420
+ running
421
+ agentLabel="Claude Code"
422
+ startup={connected}
423
+ />,
424
+ ),
425
+ ).toContain("Connected to Radar&#x27;s tools");
426
+
427
+ const ready = mergeStartupSignal(connected, readyEvent);
428
+ const html = renderToStaticMarkup(
429
+ <Timeline items={[]} running agentLabel="Claude Code" startup={ready} />,
430
+ );
431
+ expect(html).toContain(
432
+ "Claude Code ready · claude-opus-5 · 24 Radar tools",
433
+ );
434
+ expect(html).not.toContain("Starting investigation…");
435
+ expect(html).not.toContain("MCP server");
436
+ });
437
+
438
+ it("keeps the furthest phase when the handshake lands after the init line", () => {
439
+ const ready = mergeStartupSignal(undefined, readyEvent);
440
+ const late = mergeStartupSignal(ready, { phase: "connected" });
441
+ expect(late).toEqual({
442
+ phase: "ready",
443
+ model: "claude-opus-5",
444
+ toolCount: 24,
445
+ mcpServers: [{ name: "radar", status: "connected" }],
446
+ });
447
+ expect(mergeStartupSignal(ready, { phase: "investigating" })).toBe(late);
448
+ expect(mergeStartupSignal(undefined, { phase: "unknown" })).toBeUndefined();
449
+ });
450
+
451
+ it("yields to the tool activity once the transcript has items", () => {
452
+ const html = renderToStaticMarkup(
453
+ <Timeline
454
+ items={[
455
+ {
456
+ kind: "tool",
457
+ id: "tool-1",
458
+ tool: "get_logs",
459
+ status: "running",
460
+ animate: false,
461
+ },
462
+ ]}
463
+ running
464
+ agentLabel="Claude Code"
465
+ startup={mergeStartupSignal(undefined, readyEvent)}
466
+ />,
467
+ );
468
+ expect(html).toContain("Reading logs…");
469
+ expect(html).not.toContain("Claude Code ready");
470
+ });
471
+
472
+ it("shows no startup label on a replayed finished turn", () => {
473
+ const html = renderToStaticMarkup(
474
+ <Timeline
475
+ items={[]}
476
+ running={false}
477
+ agentLabel="Claude Code"
478
+ startup={mergeStartupSignal(undefined, readyEvent)}
479
+ />,
480
+ );
481
+ expect(html).not.toContain("Claude Code ready");
482
+ expect(html).not.toContain("Starting investigation…");
483
+ expect(html).not.toContain("Connected to Radar");
484
+ });
485
+
486
+ it("renders a bare ready label when the init facts are absent", () => {
487
+ // Hosted transports may forward only the phase name; the label and the
488
+ // warning must degrade to exactly what was reported.
489
+ const startup = mergeStartupSignal(undefined, { phase: "ready" });
490
+ const html = renderToStaticMarkup(
491
+ <Timeline
492
+ items={[]}
493
+ running
494
+ agentLabel="Claude Code"
495
+ startup={startup}
496
+ />,
497
+ );
498
+ expect(html).toContain("Claude Code ready</span>");
499
+ expect(html).not.toContain("Radar tools");
500
+ expect(html).not.toContain("MCP server");
501
+ });
502
+
503
+ it("uses conditional wording for servers whose startup state is not final", () => {
504
+ const startup = mergeStartupSignal(undefined, {
505
+ phase: "ready",
506
+ model: "claude-opus-5",
507
+ toolCount: 24,
508
+ mcpServers: [
509
+ { name: "radar", status: "connected" },
510
+ { name: "github", status: "pending" },
511
+ ],
512
+ });
513
+ const pending = renderToStaticMarkup(
514
+ <Timeline
515
+ items={[]}
516
+ running
517
+ agentLabel="Claude Code"
518
+ startup={startup}
519
+ />,
520
+ );
521
+ expect(pending).toContain("is pending");
522
+ expect(pending).toContain("may not have been available");
523
+ expect(pending).not.toContain("ran this turn without");
524
+
525
+ const failed = renderToStaticMarkup(
526
+ <Timeline
527
+ items={[]}
528
+ running
529
+ agentLabel="Claude Code"
530
+ startup={mergeStartupSignal(undefined, {
531
+ phase: "ready",
532
+ mcpServers: [
533
+ { name: "radar", status: "connected" },
534
+ { name: "github", status: "failed" },
535
+ ],
536
+ })}
537
+ />,
538
+ );
539
+ expect(failed).toContain("failed to connect");
540
+ expect(failed).toContain("ran this turn without those tools");
541
+ });
542
+
543
+ it("warns when the CLI reports an MCP server that is not connected", () => {
544
+ const startup = mergeStartupSignal(undefined, {
545
+ phase: "ready",
546
+ model: "claude-opus-5",
547
+ toolCount: 0,
548
+ mcpServers: [{ name: "radar", status: "failed" }],
549
+ });
550
+ const live = renderToStaticMarkup(
551
+ <Timeline
552
+ items={[]}
553
+ running
554
+ agentLabel="Claude Code"
555
+ startup={startup}
556
+ />,
557
+ );
558
+ expect(live).toContain("Claude Code ready · claude-opus-5 · 0 Radar tools");
559
+ expect(live).toContain("failed to connect");
560
+ expect(live).toContain("had no Radar tools this turn");
561
+
562
+ // The warning is a fact of the run and survives replay; the label does not.
563
+ const replayed = renderToStaticMarkup(
564
+ <Timeline
565
+ items={[{ kind: "thinking", text: "done", animate: false }]}
566
+ running={false}
567
+ agentLabel="Claude Code"
568
+ startup={startup}
569
+ />,
570
+ );
571
+ expect(replayed).toContain("failed to connect");
572
+ expect(replayed).not.toContain("Claude Code ready");
573
+ });
574
+ });
575
+
576
+ describe("running status elapsed counter", () => {
577
+ it("is a row-level figure, never part of the phase label", () => {
578
+ expect(runningElapsedLabel(0)).toBeUndefined();
579
+ expect(runningElapsedLabel(2)).toBeUndefined();
580
+ expect(runningElapsedLabel(3)).toBe("3s elapsed");
581
+ expect(runningElapsedLabel(19)).toBe("19s elapsed");
582
+
583
+ const html = renderToStaticMarkup(
584
+ <Timeline
585
+ items={[]}
586
+ running
587
+ agentLabel="Claude Code"
588
+ startup={mergeStartupSignal(undefined, { phase: "connected" })}
589
+ />,
590
+ );
591
+ const label = html.match(/<span class="ai-shimmer[^"]*">([^<]*)</);
592
+ expect(label?.[1]).toBe("Connected to Radar&#x27;s tools");
593
+ expect(label?.[1]).not.toMatch(/\d+s/);
594
+ });
595
+ });
596
+
597
+ describe("tool row duration and failure reason", () => {
598
+ const doneStep = (
599
+ overrides: Partial<Extract<TimelineItem, { kind: "tool" }>>,
600
+ ): TimelineItem => ({
601
+ kind: "tool",
602
+ id: "tool-1",
603
+ tool: "get_events",
604
+ status: "done",
605
+ summary: '{"namespace":"dev"}',
606
+ result: '{"events":[]}',
607
+ isError: false,
608
+ animate: false,
609
+ ...overrides,
610
+ });
611
+
612
+ it("hides sub-threshold durations and shows whole seconds above it", () => {
613
+ expect(toolDurationLabel(undefined)).toBeUndefined();
614
+ expect(toolDurationLabel(125)).toBeUndefined();
615
+ expect(toolDurationLabel(1999)).toBeUndefined();
616
+ expect(toolDurationLabel(2000)).toBe("2s");
617
+ expect(toolDurationLabel(3400)).toBe("3s");
618
+ expect(toolDurationLabel(12600)).toBe("13s");
619
+
620
+ const quick = renderToStaticMarkup(
621
+ <ThemeProvider>
622
+ <Timeline items={[doneStep({ ms: 125 })]} running={false} />
623
+ </ThemeProvider>,
624
+ );
625
+ // The only millisecond figure is the expanded result header, not the row.
626
+ expect(quick.match(/125ms/g)).toHaveLength(1);
627
+ expect(quick.indexOf("125ms")).toBeGreaterThan(
628
+ quick.indexOf("Original result"),
629
+ );
630
+ expect(quick).not.toMatch(/>\d+s</);
631
+
632
+ const slow = renderToStaticMarkup(
633
+ <ThemeProvider>
634
+ <Timeline items={[doneStep({ ms: 3400 })]} running={false} />
635
+ </ThemeProvider>,
636
+ );
637
+ expect(slow).toContain(">3s<");
638
+ expect(slow.indexOf(">3s<")).toBeLessThan(slow.indexOf("Original result"));
639
+ expect(slow.match(/3400ms/g)).toHaveLength(1);
640
+ });
641
+
642
+ it("keeps the exact milliseconds in the expanded result header", () => {
643
+ const sourceId = investigationEvidenceSourceId(0, "tool-1");
644
+ const html = renderToStaticMarkup(
645
+ <ThemeProvider>
646
+ <Timeline
647
+ items={[doneStep({ ms: 3400 })]}
648
+ running={false}
649
+ turnIndex={0}
650
+ sourceRevealRequest={{ sourceId, requestId: 1 }}
651
+ />
652
+ </ThemeProvider>,
653
+ );
654
+ expect(html).toContain('aria-expanded="true"');
655
+ expect(html).toContain("Original result");
656
+ expect(html).toContain("3400ms");
657
+ });
658
+
659
+ it("extracts a one-line reason from the producer's error text", () => {
660
+ expect(
661
+ toolErrorReason('\ninvalid duration "30d": time: unknown unit "d"\nmore'),
662
+ ).toBe('invalid duration "30d": time: unknown unit "d"');
663
+ expect(
664
+ toolErrorReason(JSON.stringify({ error: 'resource not found: pod "x"' })),
665
+ ).toBe('resource not found: pod "x"');
666
+ expect(toolErrorReason("")).toBeUndefined();
667
+ expect(toolErrorReason(" \n ")).toBeUndefined();
668
+ expect(toolErrorReason('{"error":""}')).toBeUndefined();
669
+ // Radar's not-found hints are for the agent; the row keeps what failed.
670
+ expect(
671
+ toolErrorReason(
672
+ 'resource not found: secret "project-infra" not found — found Deployment autopush/project-infra — retry with kind=deployment; or use search',
673
+ ),
674
+ ).toBe('resource not found: secret "project-infra" not found');
675
+ });
676
+
677
+ it("middle-truncates a long reason so its identifier tail survives", () => {
678
+ const short = 'resource not found: secret "dev/does-not-exist" not found';
679
+ expect(middleTruncate(short)).toBe(short);
680
+ const long =
681
+ "failed to get logs for autopush/project-infra-68c7b766dc-g2xlg: container is waiting to start: CreateContainerConfigError for pod project-infra-68c7b766dc-g2xlg";
682
+ const cut = middleTruncate(long);
683
+ expect(cut.length).toBeLessThanOrEqual(100);
684
+ expect(cut).toContain("…");
685
+ expect(cut.startsWith("failed to get logs for autopush")).toBe(true);
686
+ expect(cut.endsWith("project-infra-68c7b766dc-g2xlg")).toBe(true);
687
+ expect(middleTruncate("abcdefghij", 6, 2)).toBe("abc…ij");
688
+ });
689
+
690
+ it("lets the arguments give way before the reason on a collapsed failed row", () => {
691
+ const html = renderToStaticMarkup(
692
+ <ThemeProvider>
693
+ <Timeline
694
+ items={[
695
+ doneStep({
696
+ isError: true,
697
+ summary:
698
+ '{"kind":"Secret","namespace":"dev","name":"does-not-exist"}',
699
+ result:
700
+ 'resource not found: secret "dev/does-not-exist" not found — found Deployment dev/api — retry with kind=deployment',
701
+ }),
702
+ ]}
703
+ running={false}
704
+ />
705
+ </ThemeProvider>,
706
+ );
707
+ const args = html.match(/<span class="([^"]*)">kind=Secret/);
708
+ expect(args?.[1]).toContain("flex-1");
709
+ expect(args?.[1]).toContain("truncate");
710
+ const reason = html.match(
711
+ /<span class="investigation-tool-reason ([^"]*)">([^<]*)</,
712
+ );
713
+ expect(reason?.[1]).toContain("shrink-0");
714
+ expect(reason?.[2]).toBe(
715
+ "resource not found: secret &quot;dev/does-not-exist&quot; not found",
716
+ );
717
+ });
718
+
719
+ it("shows the failure reason inline on a failed row only when the producer gave one", () => {
720
+ const failed = renderToStaticMarkup(
721
+ <ThemeProvider>
722
+ <Timeline
723
+ items={[
724
+ doneStep({
725
+ isError: true,
726
+ result:
727
+ '\nfailed to get logs for dev/api: container "api" is waiting to start: CreateContainerConfigError',
728
+ }),
729
+ ]}
730
+ running={false}
731
+ />
732
+ </ThemeProvider>,
733
+ );
734
+ expect(failed).toContain("investigation-tool-reason");
735
+ expect(failed).toContain(
736
+ "failed to get logs for dev/api: container &quot;api&quot; is waiting to start",
737
+ );
738
+ expect(failed.indexOf("investigation-tool-reason")).toBeGreaterThan(
739
+ failed.indexOf("namespace=dev"),
740
+ );
741
+
742
+ const silent = renderToStaticMarkup(
743
+ <ThemeProvider>
744
+ <Timeline
745
+ items={[doneStep({ isError: true, result: undefined })]}
746
+ running={false}
747
+ />
748
+ </ThemeProvider>,
749
+ );
750
+ expect(silent).toContain('aria-label="Tool failed"');
751
+ expect(silent).not.toContain("investigation-tool-reason");
752
+
753
+ const succeeded = renderToStaticMarkup(
754
+ <ThemeProvider>
755
+ <Timeline
756
+ items={[doneStep({ isError: false, result: "plain text result" })]}
757
+ running={false}
758
+ />
759
+ </ThemeProvider>,
760
+ );
761
+ expect(succeeded).not.toContain("investigation-tool-reason");
762
+ });
763
+ });
764
+
380
765
  function renderAgent(
381
766
  agent: string,
382
767
  profiles: ExecutionProfile[],
@@ -635,6 +1020,63 @@ describe("ResultCard conclusion states", () => {
635
1020
  ]);
636
1021
  });
637
1022
 
1023
+ it("reframes the conflict banner when the agent explained every adverse card", () => {
1024
+ const html = renderToStaticMarkup(
1025
+ <ResultCard
1026
+ diagnosis={diagnosis({
1027
+ healthy: true,
1028
+ report: "The workload appears ready.",
1029
+ })}
1030
+ evidenceConflict
1031
+ evidenceConflictExplainedBy={["CrashLoopBackOff", "Error logs"]}
1032
+ />,
1033
+ );
1034
+
1035
+ // An explanation reframes Radar's finding; it never replaces it with
1036
+ // reassurance. The banner stays a warning, still says the evidence is
1037
+ // there, and points at the notes.
1038
+ expect(html).toContain(
1039
+ "Agent reports no active problem; adverse evidence remains",
1040
+ );
1041
+ expect(html).toContain("Radar captured evidence of an active problem");
1042
+ expect(html).toContain("the note on CrashLoopBackOff and Error logs");
1043
+ expect(html).not.toContain("Assessment conflicts with captured evidence");
1044
+ expect(html).toContain("border-amber-500/40");
1045
+ expect(html).not.toContain("border-emerald-500/30");
1046
+ expect(html).not.toContain("bg-accent/5");
1047
+ // Never claims a severity tier it did not check.
1048
+ expect(html).not.toContain("warning-level");
1049
+
1050
+ const unexplained = renderToStaticMarkup(
1051
+ <ResultCard
1052
+ diagnosis={diagnosis({
1053
+ healthy: true,
1054
+ report: "The workload appears ready.",
1055
+ })}
1056
+ evidenceConflict
1057
+ evidenceConflictExplainedBy={[]}
1058
+ />,
1059
+ );
1060
+ expect(unexplained).toContain(
1061
+ "Assessment conflicts with captured evidence",
1062
+ );
1063
+
1064
+ // Coverage is a separate fact from the conflict; an explanation of the
1065
+ // conflict must not quietly retire it.
1066
+ const limited = renderToStaticMarkup(
1067
+ <ResultCard
1068
+ diagnosis={diagnosis({
1069
+ healthy: true,
1070
+ report: "The workload appears ready.",
1071
+ })}
1072
+ evidenceConflict
1073
+ evidenceConflictExplainedBy={["CrashLoopBackOff"]}
1074
+ coverageLimited
1075
+ />,
1076
+ );
1077
+ expect(limited).toContain("Evidence coverage is also limited");
1078
+ });
1079
+
638
1080
  it("keeps a follow-up framed as an answer rather than a new conclusion", () => {
639
1081
  const html = renderToStaticMarkup(
640
1082
  <ResultCard
@@ -1084,4 +1526,44 @@ describe("assessment provenance disclosure", () => {
1084
1526
  'id="investigation-evidence-',
1085
1527
  );
1086
1528
  });
1529
+
1530
+ it("shows a claim here when its card was withheld, instead of counting it as visible", () => {
1531
+ // A card-placed note whose card the pane withheld renders nowhere unless
1532
+ // this row falls back to it, while the count still claimed it was on a
1533
+ // card — a receipt for something the reader cannot see.
1534
+ const investigationCase = {
1535
+ items: [
1536
+ {
1537
+ index: 0,
1538
+ role: "context",
1539
+ claim: "WITHHELD_CLAIM",
1540
+ source: { id: "s1", tool: "issues", args: "{}" },
1541
+ placement: "card",
1542
+ groupId: "g-hidden",
1543
+ },
1544
+ ],
1545
+ } as unknown as Parameters<
1546
+ typeof AssessmentSources
1547
+ >[0]["investigationCase"];
1548
+
1549
+ const hidden = renderToStaticMarkup(
1550
+ <AssessmentSources
1551
+ investigationCase={investigationCase}
1552
+ renderedGroupIds={new Set<string>()}
1553
+ onViewSource={noop}
1554
+ />,
1555
+ );
1556
+ expect(hidden).toContain("WITHHELD_CLAIM");
1557
+ expect(hidden).not.toContain("agent note on an evidence card");
1558
+
1559
+ const shown = renderToStaticMarkup(
1560
+ <AssessmentSources
1561
+ investigationCase={investigationCase}
1562
+ renderedGroupIds={new Set(["g-hidden"])}
1563
+ onViewSource={noop}
1564
+ />,
1565
+ );
1566
+ expect(shown).not.toContain("WITHHELD_CLAIM");
1567
+ expect(shown).toContain("1 agent note on an evidence card");
1568
+ });
1087
1569
  });