@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.
- package/package.json +8 -8
- package/src/App.tsx +60 -24
- package/src/RadarApp.tsx +5 -0
- package/src/api/client.authRedirect.test.ts +12 -1
- package/src/api/client.prometheusStatus.test.ts +24 -0
- package/src/api/client.ts +325 -16
- package/src/api/diagnose.ts +62 -15
- package/src/api/drain-plan.test.ts +86 -0
- package/src/api/quotas.test.ts +26 -0
- package/src/api/quotas.ts +20 -0
- package/src/api/workloadMetrics.test.ts +40 -0
- package/src/api/workloadMetrics.ts +68 -0
- package/src/components/CloudConnectFlow.tsx +179 -42
- package/src/components/CloudFunnelButton.tsx +107 -36
- package/src/components/ConnectionErrorView.test.tsx +30 -0
- package/src/components/ConnectionErrorView.tsx +31 -19
- package/src/components/SyncProgressPanel.tsx +73 -0
- package/src/components/audit/AuditView.tsx +2 -0
- package/src/components/cloudConnectHandoff.test.ts +140 -0
- package/src/components/cloudConnectHandoff.ts +124 -0
- package/src/components/diagnose/AgentCase.tsx +131 -0
- package/src/components/diagnose/DiagnoseContext.tsx +2 -1
- package/src/components/diagnose/DiagnoseSurface.test.tsx +0 -20
- package/src/components/diagnose/DiagnoseSurface.tsx +32 -195
- package/src/components/diagnose/InvestigationEvidencePane.test.tsx +783 -7
- package/src/components/diagnose/InvestigationEvidencePane.tsx +774 -133
- package/src/components/diagnose/InvestigationView.tsx +222 -5
- package/src/components/diagnose/diagnoseEvidenceTypes.ts +22 -1
- package/src/components/diagnose/investigationCase.test.tsx +1568 -0
- package/src/components/diagnose/investigationCase.ts +439 -0
- package/src/components/diagnose/investigationEvidence/adapters/changes.ts +60 -0
- package/src/components/diagnose/investigationEvidence/adapters/diagnose.test.ts +573 -0
- package/src/components/diagnose/investigationEvidence/adapters/diagnose.ts +935 -0
- package/src/components/diagnose/investigationEvidence/adapters/events.ts +96 -0
- package/src/components/diagnose/investigationEvidence/adapters/helm.test.ts +242 -0
- package/src/components/diagnose/investigationEvidence/adapters/helm.ts +208 -0
- package/src/components/diagnose/investigationEvidence/adapters/issues.ts +172 -0
- package/src/components/diagnose/investigationEvidence/adapters/logs.test.ts +283 -0
- package/src/components/diagnose/investigationEvidence/adapters/logs.ts +183 -0
- package/src/components/diagnose/investigationEvidence/adapters/permissions.test.ts +528 -0
- package/src/components/diagnose/investigationEvidence/adapters/permissions.ts +292 -0
- package/src/components/diagnose/investigationEvidence/adapters/prometheus.test.ts +1641 -0
- package/src/components/diagnose/investigationEvidence/adapters/prometheus.ts +769 -0
- package/src/components/diagnose/investigationEvidence/adapters/resource.test.ts +54 -0
- package/src/components/diagnose/investigationEvidence/adapters/resource.ts +257 -0
- package/src/components/diagnose/investigationEvidence/adapters/topology.ts +207 -0
- package/src/components/diagnose/investigationEvidence/builder.ts +418 -0
- package/src/components/diagnose/investigationEvidence/evidenceFixtures.ts +179 -0
- package/src/components/diagnose/investigationEvidence/identity.test.ts +614 -0
- package/src/components/diagnose/investigationEvidence/identity.ts +261 -0
- package/src/components/diagnose/investigationEvidence/index.ts +55 -0
- package/src/components/diagnose/investigationEvidence/observations.ts +954 -0
- package/src/components/diagnose/investigationEvidence/parse.ts +467 -0
- package/src/components/diagnose/investigationEvidence/projection.test.ts +2703 -0
- package/src/components/diagnose/investigationEvidence/types.ts +512 -0
- package/src/components/diagnose/investigationEvidenceKinds.ts +218 -0
- package/src/components/diagnose/investigationEvidencePresentation.test.ts +31 -0
- package/src/components/diagnose/investigationEvidencePresentation.ts +1 -0
- package/src/components/diagnose/investigationMetrics.test.ts +712 -0
- package/src/components/diagnose/investigationMetrics.ts +393 -0
- package/src/components/diagnose/investigationSourceFocus.ts +2 -0
- package/src/components/diagnose/investigationState.test.ts +322 -0
- package/src/components/diagnose/investigationState.ts +147 -25
- package/src/components/diagnose/parts.test.tsx +482 -0
- package/src/components/diagnose/parts.tsx +418 -41
- package/src/components/diagnose/target.test.ts +19 -0
- package/src/components/diagnose/target.ts +3 -2
- package/src/components/gitops/GitOpsView.tsx +7 -0
- package/src/components/gitops/RemoteDestinationCloudHint.tsx +26 -0
- package/src/components/resource/PrometheusCharts.overlays.test.ts +22 -0
- package/src/components/resource/PrometheusCharts.tsx +4 -3
- package/src/components/resource/PrometheusChartsGrid.render.test.tsx +99 -0
- package/src/components/resource/PrometheusChartsGrid.tsx +245 -103
- package/src/components/resource/RightsizingStrip.test.ts +38 -0
- package/src/components/resource/RightsizingStrip.tsx +15 -3
- package/src/components/resource/WorkloadMetricsHelpDialog.test.tsx +79 -0
- package/src/components/resource/WorkloadMetricsHelpDialog.tsx +145 -0
- package/src/components/resource/WorkloadMetricsSection.render.test.tsx +351 -0
- package/src/components/resource/WorkloadMetricsSection.tsx +486 -0
- package/src/components/resource/workloadMetricValues.test.ts +65 -0
- package/src/components/resource/workloadMetricValues.ts +30 -0
- package/src/components/resources/PodFilePreview.test.tsx +131 -0
- package/src/components/resources/PodFilePreview.tsx +394 -0
- package/src/components/resources/PodFilesystemModal.tsx +157 -67
- package/src/components/resources/ResourcesView.tsx +44 -6
- package/src/components/resources/renderers/NamespaceRenderer.tsx +7 -1
- package/src/components/resources/renderers/PodRenderer.tsx +4 -0
- package/src/components/resources/renderers/RolloutRenderer.tsx +15 -1
- package/src/components/resources/renderers/WorkloadRenderer.tsx +3 -0
- package/src/components/rightsizing/RightsizingScanView.tsx +21 -0
- package/src/components/settings/SettingsDialog.tsx +94 -14
- package/src/components/traffic/TrafficFlowList.tsx +229 -70
- package/src/components/traffic/TrafficGraph.tsx +6 -6
- package/src/components/traffic/TrafficView.tsx +10 -9
- package/src/components/traffic/trafficFilters.test.ts +28 -1
- package/src/components/traffic/trafficFilters.ts +31 -0
- package/src/components/workload/WorkloadView.tsx +19 -8
- package/src/context/ConnectionContext.tsx +48 -3
- package/src/context/DiagnoseCustomization.test.tsx +26 -0
- package/src/context/DiagnoseCustomization.tsx +15 -2
- package/src/index.css +17 -0
- package/src/index.ts +1 -0
- package/src/utils/navigation.ts +1 -1
- package/src/utils/shell-safe.test.ts +25 -1
- package/src/utils/shell-safe.ts +16 -0
- package/src/utils/topology-namespace.test.ts +64 -0
- package/src/utils/topology-namespace.ts +18 -0
- package/src/components/diagnose/investigationEvidence.test.ts +0 -5202
- package/src/components/diagnose/investigationEvidence.ts +0 -4784
- package/src/components/gitops/GitOpsActions.tsx +0 -1
- package/src/components/gitops/GitOpsStatusBadge.tsx +0 -1
- package/src/components/gitops/ManagedResourcesList.tsx +0 -1
- package/src/components/gitops/SyncCountdown.tsx +0 -1
- package/src/components/gitops/index.ts +0 -4
- package/src/components/helm/ManifestDiffViewer.tsx +0 -69
- package/src/components/logs/JsonLogLine.tsx +0 -1
- package/src/components/logs/LogCore.tsx +0 -2
- package/src/components/logs/useLogBuffer.ts +0 -2
- package/src/components/logs/useLogSearch.ts +0 -1
- package/src/components/resource-drawer/ResourceDrawer.tsx +0 -216
- package/src/components/resources/drawer-components.tsx +0 -1
- package/src/components/resources/renderers/AlertRenderer.tsx +0 -1
- package/src/components/resources/renderers/CNPGBackupRenderer.tsx +0 -1
- package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +0 -1
- package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +0 -1
- package/src/components/resources/renderers/CertificateRenderer.tsx +0 -1
- package/src/components/resources/renderers/CertificateRequestRenderer.tsx +0 -1
- package/src/components/resources/renderers/ChallengeRenderer.tsx +0 -1
- package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +0 -1
- package/src/components/resources/renderers/ClusterExternalSecretRenderer.tsx +0 -1
- package/src/components/resources/renderers/ClusterIssuerRenderer.tsx +0 -1
- package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +0 -1
- package/src/components/resources/renderers/ConfigMapRenderer.tsx +0 -1
- package/src/components/resources/renderers/CronJobRenderer.tsx +0 -1
- package/src/components/resources/renderers/CronWorkflowRenderer.tsx +0 -1
- package/src/components/resources/renderers/EventRenderer.tsx +0 -1
- package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +0 -1
- package/src/components/resources/renderers/ExternalSecretRenderer.tsx +0 -1
- package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +0 -1
- package/src/components/resources/renderers/GRPCRouteRenderer.tsx +0 -1
- package/src/components/resources/renderers/GatewayClassRenderer.tsx +0 -1
- package/src/components/resources/renderers/GatewayRenderer.tsx +0 -1
- package/src/components/resources/renderers/GenericRenderer.tsx +0 -1
- package/src/components/resources/renderers/GitRepositoryRenderer.tsx +0 -1
- package/src/components/resources/renderers/HTTPRouteRenderer.tsx +0 -1
- package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +0 -1
- package/src/components/resources/renderers/IngressClassRenderer.tsx +0 -1
- package/src/components/resources/renderers/IngressRenderer.tsx +0 -1
- package/src/components/resources/renderers/IstioAuthorizationPolicyRenderer.tsx +0 -1
- package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +0 -1
- package/src/components/resources/renderers/IstioGatewayRenderer.tsx +0 -1
- package/src/components/resources/renderers/IstioPeerAuthenticationRenderer.tsx +0 -1
- package/src/components/resources/renderers/IstioServiceEntryRenderer.tsx +0 -1
- package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +0 -1
- package/src/components/resources/renderers/JobRenderer.tsx +0 -1
- package/src/components/resources/renderers/KarpenterEC2NodeClassRenderer.tsx +0 -1
- package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +0 -1
- package/src/components/resources/renderers/KedaScaledJobRenderer.tsx +0 -1
- package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +0 -1
- package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +0 -1
- package/src/components/resources/renderers/KnativeConfigurationRenderer.tsx +0 -1
- package/src/components/resources/renderers/KnativeEventingRenderer.tsx +0 -1
- package/src/components/resources/renderers/KnativeFlowRenderer.tsx +0 -1
- package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +0 -1
- package/src/components/resources/renderers/KnativeRevisionRenderer.tsx +0 -1
- package/src/components/resources/renderers/KnativeRouteRenderer.tsx +0 -1
- package/src/components/resources/renderers/KnativeServiceRenderer.tsx +0 -1
- package/src/components/resources/renderers/KnativeSourceRenderer.tsx +0 -1
- package/src/components/resources/renderers/KustomizationRenderer.tsx +0 -1
- package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +0 -1
- package/src/components/resources/renderers/LeaseRenderer.tsx +0 -1
- package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +0 -1
- package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +0 -1
- package/src/components/resources/renderers/OrderRenderer.tsx +0 -1
- package/src/components/resources/renderers/PersistentVolumeRenderer.tsx +0 -1
- package/src/components/resources/renderers/PodDisruptionBudgetRenderer.tsx +0 -1
- package/src/components/resources/renderers/PodMonitorRenderer.tsx +0 -1
- package/src/components/resources/renderers/PriorityClassRenderer.tsx +0 -1
- package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +0 -1
- package/src/components/resources/renderers/ReplicaSetRenderer.tsx +0 -1
- package/src/components/resources/renderers/RuntimeClassRenderer.tsx +0 -1
- package/src/components/resources/renderers/SbomReportRenderer.tsx +0 -1
- package/src/components/resources/renderers/SealedSecretRenderer.tsx +0 -1
- package/src/components/resources/renderers/SecretRenderer.tsx +0 -1
- package/src/components/resources/renderers/SecretStoreRenderer.tsx +0 -1
- package/src/components/resources/renderers/ServiceMonitorRenderer.tsx +0 -1
- package/src/components/resources/renderers/SimpleRouteRenderer.tsx +0 -1
- package/src/components/resources/renderers/StorageClassRenderer.tsx +0 -1
- package/src/components/resources/renderers/TraefikIngressRouteRenderer.tsx +0 -1
- package/src/components/resources/renderers/VPARenderer.tsx +0 -1
- package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +0 -1
- package/src/components/resources/renderers/VeleroVSLRenderer.tsx +0 -1
- package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +0 -1
- package/src/components/resources/renderers/WebhookConfigRenderer.tsx +0 -1
- package/src/components/resources/renderers/WorkflowRenderer.tsx +0 -1
- package/src/components/resources/renderers/WorkflowTemplateRenderer.tsx +0 -1
- package/src/components/resources/renderers/argo-cells.tsx +0 -1
- package/src/components/resources/renderers/certmanager-cells.tsx +0 -1
- package/src/components/resources/renderers/cnpg-cells.tsx +0 -1
- package/src/components/resources/renderers/eso-cells.tsx +0 -1
- package/src/components/resources/renderers/flux-cells.tsx +0 -1
- package/src/components/resources/renderers/index.ts +0 -94
- package/src/components/resources/renderers/istio-cells.tsx +0 -1
- package/src/components/resources/renderers/karpenter-cells.tsx +0 -1
- package/src/components/resources/renderers/keda-cells.tsx +0 -1
- package/src/components/resources/renderers/knative-cells.tsx +0 -1
- package/src/components/resources/renderers/kyverno-cells.tsx +0 -1
- package/src/components/resources/renderers/prometheus-cells.tsx +0 -1
- package/src/components/resources/renderers/traefik-cells.tsx +0 -1
- package/src/components/resources/renderers/trivy-cells.tsx +0 -1
- package/src/components/resources/renderers/trivy-shared.tsx +0 -1
- package/src/components/resources/renderers/velero-cells.tsx +0 -1
- package/src/components/resources/resource-utils-argo.ts +0 -2
- package/src/components/resources/resource-utils-certmanager.ts +0 -2
- package/src/components/resources/resource-utils-eso.ts +0 -2
- package/src/components/resources/resource-utils-flux.ts +0 -2
- package/src/components/resources/resource-utils-istio.ts +0 -2
- package/src/components/resources/resource-utils-karpenter.ts +0 -2
- package/src/components/resources/resource-utils-knative.ts +0 -2
- package/src/components/resources/resource-utils-kyverno.ts +0 -2
- package/src/components/resources/resource-utils-prometheus.ts +0 -2
- package/src/components/resources/resource-utils-traefik.ts +0 -1
- package/src/components/resources/resource-utils-trivy.ts +0 -2
- package/src/components/resources/resource-utils-velero.ts +0 -2
- package/src/components/shared/EditableYamlView.tsx +0 -24
- package/src/components/timeline/DiffViewer.tsx +0 -1
- package/src/components/traffic/index.ts +0 -3
- package/src/components/ui/ForceDeleteConfirmDialog.tsx +0 -1
- package/src/components/ui/MetricsChart.tsx +0 -1
- 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'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'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 "dev/does-not-exist" 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 "api" 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
|
});
|