Let a clean full Patrol clear the stale coverage caveat (#2697)

Stacked on #2686 (which is stacked on #2682). It lands after them.

## Problem

Patrol > Activity > Finding options and history > Open work showed
**Check needed: Run Patrol to check everything and refresh open work.**
whenever `overall_health.factors` held any `coverage` factor. After a
clean full run, the backend keeps its `Recent Patrol errors` coverage
factor if an earlier run in its 24-hour window errored
(`summarizeRecentPatrolCoverage`). It drops the factor only when three
clean full runs lead that window or the errored runs age out. At the
default 6-hour interval that left about 12 hours of "Check needed" after
a clean check of every resource, while the header read `Last check: 4m
ago · checked 1377 resources`. One press of Check now did not clear it.
At impact 20 or 30 the factor lowers the grade to B or C, so the same
stale signal showed **Patrol needs review** instead.

## Change

- `getPatrolFindingsEmptyState` reads run history first, through the new
`getPatrolRunCoverage`:
- **complete**: the latest completed full run ended cleanly, checked
resources, finished within the last 24 hours (the backend's window), and
no run after it failed. A coverage factor no longer withholds the
all-clear, and a grade lowered only by coverage factors no longer counts
as degraded health.
- **incomplete**: the latest full run, or a run after it, ended with
errors, or only targeted or follow-up runs have completed. Shows "Check
needed". A failed targeted run after the last clean full run is new
here: it used to read green whenever the backend had dropped its factor.
- **unproven**: no completed run, or a clean full run that checked
nothing or finished more than 24 hours ago. The coverage factor decides,
as before.
- `getPatrolVerificationPresentation` was read only for its warning
tone. `getPatrolRunCoverage` replaces it. The activity-mix label had no
reader, so it goes, along with `getPatrolActivityBreakdown` /
`formatPatrolActivityBreakdown`, whose only caller it was.
- The backend factor and its three-run rule (documented in ai-runtime)
are unchanged.
- patrol-intelligence, api-contracts and frontend-primitives now
describe the reconciliation.

## Verification

- Vitest over `src/utils src/features src/components/AI
src/components/patrol src/components/shared src/pages src/hooks
src/__tests__`: all pass except `agentInstallCommand.test.ts`, which
fails on the Mac regardless of this change, and one `DockerHostsTable`
case that timed out under load and passes on its own. The new
empty-state cases fail against the parent tree.
- Browser: embedded builds of the parent (#2686) and this tree on
private DEMO_MODE stacks, with run history and overall health stubbed,
at desktop 1440x1000 and phone 390x844. Eleven scenarios per width,
including Check now while the factor is present. Results are in the
receipt.
This commit is contained in:
rcourtman
2026-10-08 10:47:26 +01:00
committed by GitHub
12 changed files with 629 additions and 948 deletions
@@ -3270,8 +3270,8 @@ a new API state machine, queue contract, or verification-accounting field.
without adding a new API payload or page-level proof surface; those process details
remain available in expanded context, selected run records, Assistant
handoff context, or API diagnostics when they are actually needed
and the Patrol verification posture derived from run history (`getPatrolVerificationPresentation`), which today only withholds the current-findings empty state's all-clear when the latest completed full patrol ended with errors or only targeted or follow-up runs completed; the page shows no aggregate verification-posture summary (expanded run-history entries still show each run's own scope and coverage), and any surface that adds one must take it from that run-history posture rather than page-local heuristics
and the same-day activity-mix label that posture computes from the same governed run history, which no surface renders today; one that does must explain a recent full patrol followed by alert-triggered or anomaly-triggered scoped work from that label instead of reconstructing the mix from page-local timing heuristics
and the Patrol run-history coverage verdict (`getPatrolRunCoverage`), which the current-findings empty state reads before any overall-health `coverage` factor: a latest completed full patrol that ended with errors or was followed by a failed run, or only targeted or follow-up runs, withholds the all-clear, and a clean full patrol with non-zero `resources_checked` that completed within the last 24 hours, with no failed run after it, keeps a coverage factor from withholding it; the page shows no aggregate verification-posture summary (expanded run-history entries still show each run's own scope and coverage), and any surface that adds one must take it from that run-history verdict rather than page-local heuristics
and any same-day activity-mix explanation, which nothing computes or renders today; a surface that adds one must explain a recent full patrol followed by alert-triggered or anomaly-triggered scoped work from the same governed run history instead of reconstructing the mix from page-local timing heuristics
and the Patrol status recency split, so `last_patrol_at` remains reserved for completed full Patrol sweeps while scoped runs and verification checks advance `last_activity_at` without claiming a fresh full-estate verification pass
and the monitor-context Patrol coverage posture boundary, so Proxmox
overview and other monitor-first launch pages must not consume API-owned
@@ -4210,13 +4210,18 @@ the authoritative analysis outcome.
new API surface. The label stays optional when coverage is zero and reads
`checked N resources` for any completed run, whatever its type or outcome;
it never says verified.
The Patrol current-findings empty state does not reconcile AI summary
coverage factors against run history: outside runtime and stale-coverage
states, any `coverage` factor in `overall_health` withholds the all-clear,
even after a successful full run
with non-zero `resources_checked`, and run history adds its own caveat when
the latest completed full run ended with errors or only targeted or
follow-up runs completed.
The Patrol current-findings empty state reconciles AI summary coverage
factors against run history: outside runtime and stale-coverage states,
the latest completed full run decides first. One that ended with errors
or was followed by a failed run, or only targeted or follow-up runs,
withholds the all-clear. A clean one
with non-zero `resources_checked` that completed within the last 24 hours,
with no failed run after it, lets the all-clear stand even while
`overall_health` still carries a `coverage` factor such as `Recent Patrol
errors` (which the backend keeps until three consecutive clean full runs
lead its window or the errored runs leave it), and a grade lowered only by
coverage factors does not read as degraded health. In every other case a
`coverage` factor still withholds the all-clear.
and the Assistant finding-context request contract, so `/api/ai/chat`
payloads carrying `finding_id` may hydrate a structured investigation
summary from the unified finding, but raw action commands must stay
@@ -6862,9 +6862,13 @@ run history it uses `Last check` for the `last_patrol_at` full-sweep transport
fact and `Last activity` when `last_activity_at` is newer, and it never
collapses them into a generic `Last run` label. Its coverage phrase reads
`checked N resources` and never says `verified`.
If a summary shell returns, it should not present `Recent coverage is
incomplete` when run history shows a successful full patrol with non-zero
resource coverage.
The current-findings empty state already applies that run-history rule
through `getPatrolRunCoverage`: a successful latest full patrol with non-zero
resource coverage, completed within the last 24 hours with no failed run
after it, keeps an overall-health `coverage` factor from turning the empty
queue into `Check needed` or `Patrol needs review`. If a summary shell
returns, it must not present `Recent coverage is incomplete` in that state
either.
That same Patrol shell ownership includes refresh affordance state:
`frontend-modern/src/features/patrol/usePatrolIntelligenceState.ts` must keep
operator refresh controls generation-aware, timeout-bounded, and separate from
@@ -851,20 +851,27 @@ clear`, `Found N new issues`, `Fixed N issues`, `N issues still open`, or
tolerance and otherwise allows two configured Patrol intervals before
warning, so deliberately slower schedules are not mislabeled while an old
default-schedule result cannot remain green indefinitely.
No assessment readout renders today, so nothing reconciles a coverage
caveat against run history: once runtime states (running, blocked,
disabled, unavailable) and stale coverage have taken precedence, the
current-findings empty state withholds the all-clear (`Check needed`)
whenever the overall-health summary carries any
`coverage` factor, including the backend `Recent Patrol errors` factor that
can outlast a successful full Patrol run until three consecutive clean full
runs lead the coverage window or the erroring runs leave it
(`summarizeRecentPatrolCoverage` in `internal/ai/intelligence.go`), and
whenever the latest completed full run ended with errors or only targeted or
follow-up runs completed. If an assessment readout returns, its coverage
caveat must reconcile against current run-history proof: a stale coverage
factor or prediction must not claim recent coverage is incomplete when the
latest completed full Patrol run successfully checked real resources.
Coverage caveats reconcile against run history before the health summary.
Once runtime states (running, blocked, disabled, unavailable) and stale
coverage have taken precedence, the current-findings empty state reads the
latest completed full Patrol run first (`getPatrolRunCoverage`). When that
run or any run after it ended with errors, or only targeted or follow-up
runs have completed, the empty state withholds the all-clear
(`Check needed`). When it ended
without errors, checked real resources, finished within the last 24 hours,
and no run after it failed, a `coverage` factor in the overall-health
summary does not withhold the all-clear, and a grade lowered by nothing but
coverage factors does not read as `Patrol needs review`. The backend keeps
its `Recent Patrol errors` factor until three consecutive clean full runs
lead its 24-hour coverage window or the erroring runs leave it
(`summarizeRecentPatrolCoverage` in `internal/ai/intelligence.go`), so
without that reconciliation a clean check would keep reading `Check needed`
until up to two more clean full runs followed it, about 12 hours at the
default 6-hour interval. The summary's coverage factor still decides
whenever run history cannot vouch for current coverage: no completed run,
or a clean full run that checked no resources or finished more than 24
hours ago. An assessment readout that returns must apply the same
reconciliation.
Recency coverage copy comes from the shared presentation helper
(`getPatrolRecencyPresentation`): `checked N resources` for the latest
completed run with a positive count, whatever its type or error state. It
@@ -1754,8 +1761,8 @@ have had no production caller since the Patrol summary card was deleted
(several lost theirs months earlier) and are gone;
`frontend-modern/src/utils/patrolSummaryPresentation.ts` now owns only the
header recency line (`getPatrolRecencyPresentation`) and the run-history
verification posture the current-findings empty state reads
(`getPatrolVerificationPresentation`). If a summary card, metric strip, or
coverage verdict the current-findings empty state reads
(`getPatrolRunCoverage`). If a summary card, metric strip, or
assessment readout returns, it must derive its state from one shared
Patrol-owned presenter rather than page-local logic, and keep these rules.
A secondary metric strip must not render `No issues found` while the governed
@@ -1905,12 +1912,15 @@ split also applies to findings counts: the primary assessment card may keep
health as supporting context, but active
findings, warning counts, and critical counts belong to the supporting metric
strip rather than being repeated as duplicate badges inside the primary card.
Patrol renders no check summary today. `getPatrolVerificationPresentation`
still classifies recent run history (a clean full check, a full check that
ended with errors, only targeted or follow-up checks, or no completed check)
and computes a same-day activity-mix label, but its only reader is the
current-findings empty state, which uses just its warning tone to withhold the
all-clear; the header recency line carries the visible coverage phrase. If a
Patrol renders no check summary today. `getPatrolRunCoverage` reduces run
history to the coverage verdict the current-findings empty state reads:
complete (the latest completed full check ended cleanly, checked resources,
finished within the last 24 hours, and no check after it failed), incomplete
(it or a later check ended with errors, or only targeted or follow-up checks
completed), or unproven. Nothing computes a same-day activity mix:
`getPatrolActivityBreakdown` and its formatter went with the verification
presenter that was their only caller. The header recency line carries the
visible coverage phrase. If a
check summary returns with that summary surface, it must explain what Patrol
actually checked without turning coverage mechanics into the default product
language: whether Patrol recently completed a clean broad check, only ran
@@ -0,0 +1,43 @@
{
"version": 1,
"base_sha": "421d1e5423e5b83b7923ea8c3b133ad520cd5f60",
"verified_at": "2026-10-07T16:15:25Z",
"result": "passed",
"changed_paths": [
"frontend-modern/src/utils/patrolEmptyStatePresentation.ts",
"frontend-modern/src/utils/patrolRunPresentation.ts",
"frontend-modern/src/utils/patrolSummaryPresentation.ts"
],
"content_sha256": {
"frontend-modern/src/utils/patrolEmptyStatePresentation.ts": "8e898df44f5a49293290bcc853096bbd8822c3a686193390df3e0e46b06a26ef",
"frontend-modern/src/utils/patrolRunPresentation.ts": "a3bce8de3270dfdc3f9a38b3b9d285c2d938f2a4f952f60174ba4689ce7a71c3",
"frontend-modern/src/utils/patrolSummaryPresentation.ts": "6c9dab2e7009cd3a05125f7bf131c45e0560f97d37953fc799cbf9349a7bf9f3"
},
"routes": [
"/patrol",
"/patrol (Activity > Finding options and history > Open work)"
],
"viewports": [
{
"width": 1440,
"height": 1000
},
{
"width": 390,
"height": 844
}
],
"states": [
"Embedded production builds (vite build into internal/api/frontend-modern/dist, go build) of the parent tree (421d1e5423) and this tree, each on a private DEMO_MODE mock stack with a fresh data dir; the same Playwright script ran against both, with findings, run history and overall_health stubbed via page.route.",
"Open work empty state, this tree: grade A + 'Recent Patrol errors' coverage factor after a clean full run (1377 resources) with an older errored run -> 'No current issues / Checked 1377 resources.' (parent: 'Check needed'); same at grade C with impact -0.3 -> 'No current issues' (parent: 'Check needed'); coverage factor plus a prediction factor at grade C -> 'Patrol needs review' (parent: 'Check needed').",
"Caveat still surfaces in this tree and the parent: latest full run ended with errors, clean full run with 0 resources, only a targeted run, a failed targeted run after the clean full run (with the factor), and a 49-hour-old clean full run followed by a fresh targeted run under a 'Patrol coverage incomplete' factor -> 'Check needed / Run Patrol to check everything and refresh open work.'",
"Failed targeted run after the clean full run with no coverage factor: this tree 'Check needed', parent 'No current issues' (intended change).",
"Clean full run with no factors -> 'No current issues / Checked 1377 resources.' in both trees.",
"No horizontal overflow and no console errors in any of the 22 scenario x viewport cases per tree."
],
"interactions": [
"Demo sign-in, navigate to /patrol, open the Activity tab, expand 'Finding options and history', read the Open work empty state; desktop mouse and phone touch contexts.",
"Check now from the header while the latest full run had errors and the factor was present (POST /api/ai/patrol/run stubbed, the new clean run then returned by run history, factor kept as the backend keeps it): this tree changed 'Check needed' to 'No current issues / Checked 1377 resources.' after acceptance reconciliation; the parent stayed 'Check needed'.",
"Screenshots of every empty state captured and inspected at both widths."
]
}
@@ -99,6 +99,269 @@ describe('patrolEmptyStatePresentation', () => {
});
});
describe('coverage factor after a clean full run', () => {
// Ten minutes after the clean full run below completed.
const NOW = Date.parse('2026-10-07T12:15:00Z');
const recentErrorsFactor = (impact: number) => ({
name: 'Recent Patrol errors',
impact,
description:
'Recent Patrol runs encountered errors, so the current health summary may be incomplete.',
category: 'coverage',
});
const fullRun = (overrides: Partial<PatrolRunRecord>) =>
({
id: 'run-full',
started_at: '2026-10-07T12:00:00Z',
completed_at: '2026-10-07T12:05:00Z',
type: 'patrol',
status: 'healthy',
error_count: 0,
resources_checked: 1377,
finding_ids: [],
...overrides,
}) as unknown as PatrolRunRecord;
const erroredEarlierRun = fullRun({
id: 'run-errored-earlier',
started_at: '2026-10-07T06:00:00Z',
completed_at: '2026-10-07T06:05:00Z',
status: 'error',
error_count: 2,
});
it('reads as no current issues once the latest full run checked resources cleanly', () => {
expect(
getPatrolFindingsEmptyState({
filter: 'active',
overallHealth: {
score: 90,
grade: 'A',
trend: 'stable',
factors: [recentErrorsFactor(-0.1)],
prediction:
'Recent Patrol runs encountered errors, so the current health summary may be incomplete.',
},
runtimeState: 'active',
nowMs: NOW,
runs: [fullRun({}), erroredEarlierRun],
}),
).toEqual({
title: 'No current issues',
body: 'Checked 1377 resources.',
tone: 'success',
});
});
it('does not ask for review when the coverage factor alone lowered the grade', () => {
expect(
getPatrolFindingsEmptyState({
filter: 'active',
overallHealth: {
score: 70,
grade: 'C',
trend: 'stable',
factors: [
recentErrorsFactor(-0.3),
{
name: 'Knowledge learned',
impact: 0.05,
description: 'Pulse Patrol has learned about 12 resources',
category: 'learning',
},
],
prediction:
'Most recent Patrol runs encountered errors (6 of 10); the current health summary is not reliable until coverage stabilizes.',
},
runtimeState: 'active',
nowMs: NOW,
runs: [fullRun({}), erroredEarlierRun],
}),
).toEqual({
title: 'No current issues',
body: 'Checked 1377 resources.',
tone: 'success',
});
});
it('still asks for review when another factor lowered the grade', () => {
expect(
getPatrolFindingsEmptyState({
filter: 'active',
overallHealth: {
score: 70,
grade: 'C',
trend: 'declining',
factors: [
recentErrorsFactor(-0.2),
{
name: 'Predicted issue',
impact: -0.1,
description: 'disk_full predicted within 2.0 days',
category: 'prediction',
},
],
prediction: 'Recent Patrol runs encountered errors (2 of 5).',
},
runtimeState: 'active',
nowMs: NOW,
runs: [fullRun({}), erroredEarlierRun],
}),
).toEqual({
title: 'Patrol needs review',
body: 'No current issues are listed, but Patrol health needs review.',
tone: 'warning',
});
});
it('keeps the coverage caveat when the clean full run checked no resources', () => {
expect(
getPatrolFindingsEmptyState({
filter: 'active',
overallHealth: {
score: 90,
grade: 'A',
trend: 'stable',
factors: [recentErrorsFactor(-0.1)],
prediction: '',
},
runtimeState: 'active',
nowMs: NOW,
runs: [fullRun({ resources_checked: 0 }), erroredEarlierRun],
}),
).toEqual({
title: 'Check needed',
body: 'Run Patrol to check everything and refresh open work.',
tone: 'warning',
});
});
it('keeps the coverage caveat when only targeted runs completed', () => {
expect(
getPatrolFindingsEmptyState({
filter: 'active',
overallHealth: {
score: 100,
grade: 'A',
trend: 'stable',
factors: [],
prediction: 'Infrastructure is healthy with no significant issues detected.',
},
runtimeState: 'active',
nowMs: NOW,
runs: [fullRun({ type: 'scoped', resources_checked: 3 })],
}),
).toEqual({
title: 'Check needed',
body: 'Run Patrol to check everything and refresh open work.',
tone: 'warning',
});
});
it('keeps the coverage caveat when a targeted run failed after the clean full run', () => {
expect(
getPatrolFindingsEmptyState({
filter: 'active',
overallHealth: {
score: 90,
grade: 'A',
trend: 'stable',
factors: [recentErrorsFactor(-0.1)],
prediction: '',
},
runtimeState: 'active',
nowMs: NOW,
runs: [
fullRun({
id: 'run-scoped-failed',
type: 'scoped',
started_at: '2026-10-07T12:08:00Z',
completed_at: '2026-10-07T12:09:00Z',
resources_checked: 0,
status: 'error',
error_count: 1,
}),
fullRun({}),
],
}),
).toEqual({
title: 'Check needed',
body: 'Run Patrol to check everything and refresh open work.',
tone: 'warning',
});
});
it('asks for a check when a targeted run failed after the clean full run, even without a coverage factor', () => {
expect(
getPatrolFindingsEmptyState({
filter: 'active',
overallHealth: {
score: 100,
grade: 'A',
trend: 'stable',
factors: [],
prediction: 'Infrastructure is healthy with no significant issues detected.',
},
runtimeState: 'active',
nowMs: NOW,
runs: [
fullRun({
id: 'run-scoped-failed',
type: 'scoped',
started_at: '2026-10-07T12:08:00Z',
completed_at: '2026-10-07T12:09:00Z',
resources_checked: 0,
status: 'error',
error_count: 1,
}),
fullRun({}),
],
}),
).toEqual({
title: 'Check needed',
body: 'Run Patrol to check everything and refresh open work.',
tone: 'warning',
});
});
it('keeps the coverage caveat when the clean full run is older than a day', () => {
expect(
getPatrolFindingsEmptyState({
filter: 'active',
overallHealth: {
score: 80,
grade: 'B',
trend: 'stable',
factors: [
{
name: 'Patrol coverage incomplete',
impact: -0.2,
description:
'Recent Patrol activity only covered targeted checks. Run Patrol to check everything.',
category: 'coverage',
},
],
prediction: '',
},
runtimeState: 'active',
nowMs: NOW + 2 * 24 * 60 * 60 * 1000,
runs: [
fullRun({
id: 'run-scoped-recent',
type: 'scoped',
started_at: '2026-10-09T12:00:00Z',
completed_at: '2026-10-09T12:01:00Z',
resources_checked: 2,
}),
fullRun({}),
],
}),
).toEqual({
title: 'Check needed',
body: 'Run Patrol to check everything and refresh open work.',
tone: 'warning',
});
});
});
it('uses an attention-focused empty state when patrol health is degraded for non-coverage reasons', () => {
expect(
getPatrolFindingsEmptyState({
@@ -2,8 +2,6 @@ import { describe, expect, it } from 'vitest';
import type { PatrolRunRecord, PatrolTriggerStatus } from '@/api/patrol';
import {
formatPatrolActivityBreakdown,
getPatrolActivityBreakdown,
getPatrolLatestRunPresentation,
getPatrolRunCoverageSummary,
getPatrolRunOperatorRecordPresentation,
@@ -11,9 +9,6 @@ import {
getPatrolRunResourcesHeading,
getPatrolTriggerStatusSummary,
} from '@/utils/patrolRunPresentation';
import type { PatrolActivityBreakdown } from '@/utils/patrolRunPresentation';
const REFERENCE_DATE = new Date('2026-03-12T12:00:00Z');
const makeRun = (overrides: Partial<PatrolRunRecord> = {}): PatrolRunRecord => ({
id: 'run-1',
@@ -404,85 +399,6 @@ describe('getPatrolRunRecordSummaryPresentation (branch coverage)', () => {
});
});
describe('formatPatrolActivityBreakdown (branch coverage)', () => {
it('formats alert-cleared segment', () => {
expect(
formatPatrolActivityBreakdown({
totalRuns: 1,
fullPatrols: 0,
verificationChecks: 0,
alertTriggeredRuns: 0,
anomalyTriggeredRuns: 0,
alertClearedRuns: 1,
otherScopedRuns: 0,
newFindings: 0,
} satisfies PatrolActivityBreakdown),
).toBe('1 alert-cleared check');
});
it('formats verification segment', () => {
expect(
formatPatrolActivityBreakdown({
totalRuns: 1,
fullPatrols: 0,
verificationChecks: 1,
alertTriggeredRuns: 0,
anomalyTriggeredRuns: 0,
alertClearedRuns: 0,
otherScopedRuns: 0,
newFindings: 0,
} satisfies PatrolActivityBreakdown),
).toBe('1 follow-up check');
});
it('formats other-scoped segment', () => {
expect(
formatPatrolActivityBreakdown({
totalRuns: 1,
fullPatrols: 0,
verificationChecks: 0,
alertTriggeredRuns: 0,
anomalyTriggeredRuns: 0,
alertClearedRuns: 0,
otherScopedRuns: 1,
newFindings: 0,
} satisfies PatrolActivityBreakdown),
).toBe('1 targeted check');
});
it('returns empty string when all segments are zero', () => {
expect(
formatPatrolActivityBreakdown({
totalRuns: 0,
fullPatrols: 0,
verificationChecks: 0,
alertTriggeredRuns: 0,
anomalyTriggeredRuns: 0,
alertClearedRuns: 0,
otherScopedRuns: 0,
newFindings: 0,
} satisfies PatrolActivityBreakdown),
).toBe('');
});
it('renders all segments in canonical order with plurals', () => {
expect(
formatPatrolActivityBreakdown({
totalRuns: 8,
fullPatrols: 2,
verificationChecks: 2,
alertTriggeredRuns: 2,
anomalyTriggeredRuns: 1,
alertClearedRuns: 1,
otherScopedRuns: 1,
newFindings: 0,
} satisfies PatrolActivityBreakdown),
).toBe(
'2 full checks, 2 alert-triggered checks, 1 anomaly-triggered check, 1 alert-cleared check, 2 follow-up checks, 1 targeted check',
);
});
});
describe('getPatrolTriggerStatusSummary (branch coverage)', () => {
it('returns undefined when status is undefined', () => {
expect(getPatrolTriggerStatusSummary(undefined)).toBeUndefined();
@@ -552,135 +468,6 @@ describe('getPatrolTriggerStatusSummary (branch coverage)', () => {
});
});
describe('getPatrolActivityBreakdown + isSameLocalDay (branch coverage)', () => {
it('counts alert-cleared triggered scoped runs', () => {
const result = getPatrolActivityBreakdown(
[
makeRun({
id: 'cleared-1',
type: 'scoped',
trigger_reason: 'alert_cleared',
}),
],
REFERENCE_DATE,
);
expect(result.alertClearedRuns).toBe(1);
expect(result.totalRuns).toBe(1);
});
it('counts verification-type runs as follow-up checks', () => {
const result = getPatrolActivityBreakdown(
[
makeRun({
id: 'verif-1',
type: 'verification',
trigger_reason: 'alert_fired',
}),
],
REFERENCE_DATE,
);
expect(result.verificationChecks).toBe(1);
expect(result.alertTriggeredRuns).toBe(0);
});
it('counts unknown trigger reasons as other scoped runs', () => {
const result = getPatrolActivityBreakdown(
[
makeRun({
id: 'manual-1',
type: 'scoped',
trigger_reason: 'manual',
}),
],
REFERENCE_DATE,
);
expect(result.otherScopedRuns).toBe(1);
});
it('counts scoped runs with no trigger reason as other scoped runs', () => {
const result = getPatrolActivityBreakdown(
[
makeRun({
id: 'no-trigger',
type: 'scoped',
trigger_reason: undefined,
}),
],
REFERENCE_DATE,
);
expect(result.otherScopedRuns).toBe(1);
});
it('skips runs on a different local day', () => {
const result = getPatrolActivityBreakdown(
[
makeRun({
id: 'yesterday',
started_at: '2026-03-11T10:00:00Z',
completed_at: '2026-03-11T10:01:00Z',
type: 'patrol',
}),
],
REFERENCE_DATE,
);
expect(result.totalRuns).toBe(0);
expect(result.fullPatrols).toBe(0);
});
it('skips runs with an invalid started_at timestamp (isSameLocalDay invalid-date branch)', () => {
const result = getPatrolActivityBreakdown(
[
makeRun({
id: 'bad-date',
started_at: 'not-a-valid-date',
}),
],
REFERENCE_DATE,
);
expect(result.totalRuns).toBe(0);
});
it('skips runs with an empty started_at string (isSameLocalDay empty-timestamp branch)', () => {
const result = getPatrolActivityBreakdown(
[
makeRun({
id: 'empty-date',
started_at: '',
} as unknown as PatrolRunRecord),
],
REFERENCE_DATE,
);
expect(result.totalRuns).toBe(0);
});
it('clamps negative new_findings to zero', () => {
const result = getPatrolActivityBreakdown(
[
makeRun({
id: 'neg-findings',
type: 'patrol',
new_findings: -5,
}),
],
REFERENCE_DATE,
);
expect(result.totalRuns).toBe(1);
expect(result.newFindings).toBe(0);
});
it('accumulates new findings across multiple same-day runs', () => {
const result = getPatrolActivityBreakdown(
[
makeRun({ id: 'a', type: 'patrol', new_findings: 2 }),
makeRun({ id: 'b', type: 'patrol', new_findings: 3 }),
],
REFERENCE_DATE,
);
expect(result.newFindings).toBe(5);
expect(result.fullPatrols).toBe(2);
});
});
describe('getPatrolRunResourcesHeading (branch coverage)', () => {
it('falls back to plain count when scoped resources exist but nothing was checked', () => {
expect(
@@ -1,8 +1,6 @@
import { describe, expect, it } from 'vitest';
import type { PatrolRunRecord } from '@/api/patrol';
import {
formatPatrolActivityBreakdown,
getPatrolActivityBreakdown,
getPatrolLatestRunPresentation,
getPatrolRunOperatorRecordPresentation,
getPatrolRunRecordSummaryPresentation,
@@ -415,111 +413,6 @@ describe('patrolRunPresentation', () => {
).toBe('Checked 0 of 2 scoped resources');
});
it('summarizes today activity by full patrol versus trigger source', () => {
const summary = getPatrolActivityBreakdown(
[
{
id: 'full-1',
started_at: '2026-03-12T08:00:00Z',
completed_at: '2026-03-12T08:05:00Z',
duration_ms: 300000,
type: 'patrol',
resources_checked: 58,
nodes_checked: 0,
guests_checked: 0,
docker_checked: 0,
storage_checked: 0,
hosts_checked: 0,
truenas_checked: 0,
pbs_checked: 0,
pmg_checked: 0,
kubernetes_checked: 0,
new_findings: 1,
existing_findings: 0,
rejected_findings: 0,
resolved_findings: 0,
auto_fix_count: 0,
findings_summary: 'ok',
finding_ids: [],
error_count: 0,
status: 'healthy',
triage_flags: 0,
tool_call_count: 0,
},
{
id: 'alert-1',
started_at: '2026-03-12T09:00:00Z',
completed_at: '2026-03-12T09:01:00Z',
duration_ms: 60000,
type: 'scoped',
trigger_reason: 'alert_fired',
resources_checked: 1,
nodes_checked: 0,
guests_checked: 0,
docker_checked: 0,
storage_checked: 0,
hosts_checked: 0,
truenas_checked: 0,
pbs_checked: 0,
pmg_checked: 0,
kubernetes_checked: 0,
new_findings: 0,
existing_findings: 0,
rejected_findings: 0,
resolved_findings: 0,
auto_fix_count: 0,
findings_summary: 'ok',
finding_ids: [],
error_count: 0,
status: 'healthy',
triage_flags: 0,
tool_call_count: 0,
},
{
id: 'anomaly-1',
started_at: '2026-03-12T10:00:00Z',
completed_at: '2026-03-12T10:01:00Z',
duration_ms: 60000,
type: 'scoped',
trigger_reason: 'anomaly',
resources_checked: 1,
nodes_checked: 0,
guests_checked: 0,
docker_checked: 0,
storage_checked: 0,
hosts_checked: 0,
truenas_checked: 0,
pbs_checked: 0,
pmg_checked: 0,
kubernetes_checked: 0,
new_findings: 2,
existing_findings: 0,
rejected_findings: 0,
resolved_findings: 0,
auto_fix_count: 0,
findings_summary: 'ok',
finding_ids: [],
error_count: 0,
status: 'healthy',
triage_flags: 0,
tool_call_count: 0,
},
] satisfies PatrolRunRecord[],
new Date('2026-03-12T12:00:00Z'),
);
expect(summary).toMatchObject({
totalRuns: 3,
fullPatrols: 1,
alertTriggeredRuns: 1,
anomalyTriggeredRuns: 1,
newFindings: 3,
});
expect(formatPatrolActivityBreakdown(summary)).toBe(
'1 full check, 1 alert-triggered check, 1 anomaly-triggered check',
);
});
it('returns canonical patrol run loading and unavailable copy', () => {
expect(getRunHistoryLoadingState()).toBe('Loading history...');
expect(getToolCallsLoadingState()).toBe('Loading tool calls...');
@@ -4,9 +4,12 @@ import type { PatrolRunRecord } from '@/api/patrol';
import {
getPatrolRecencyPresentation,
getPatrolVerificationPresentation,
getPatrolRunCoverage,
} from '@/utils/patrolSummaryPresentation';
// Ten minutes after the default fixture run completed.
const NOW = Date.parse('2026-07-10T09:15:00Z');
// Minimal typed PatrolRunRecord fixtures.
function makeRun(overrides: Partial<PatrolRunRecord> = {}): PatrolRunRecord {
return {
@@ -49,269 +52,190 @@ function successfulFullRun(resourcesChecked = 50): PatrolRunRecord {
}
// ===========================================================================
// getPatrolVerificationPresentation — runtime-state branches, full-run-with-
// errors, zero-resource full run, limited-run variants (verification/scoped/
// unknown), no-completed-runs, and getVerificationActivityMixLabel undefined
// paths.
// getPatrolRunCoverage — the latest completed full run decides; errors,
// zero-resource runs, limited-only history, and missing history.
// ===========================================================================
describe('getPatrolVerificationPresentation — runtime state', () => {
it('maps the blocked state to the paused runtime presentation', () => {
expect(getPatrolVerificationPresentation({ runtimeState: 'blocked' })).toEqual({
title: 'Patrol paused',
description: 'Patrol cannot check infrastructure until the blocking condition is cleared.',
compactLabel: 'Patrol paused',
tone: 'warning',
});
describe('getPatrolRunCoverage — latest completed full run', () => {
it('is complete when the full run ended cleanly and checked resources', () => {
expect(getPatrolRunCoverage([successfulFullRun(58)], NOW)).toBe('complete');
});
it('maps the disabled state to the disabled runtime presentation', () => {
expect(getPatrolVerificationPresentation({ runtimeState: 'disabled' })).toEqual({
title: 'Patrol disabled',
description: 'Enable Patrol to resume checks.',
compactLabel: 'Patrol disabled',
tone: 'info',
});
it('is complete when the full run checked a single resource', () => {
expect(getPatrolRunCoverage([successfulFullRun(1)], NOW)).toBe('complete');
});
it('maps the unavailable state to the unavailable runtime presentation', () => {
expect(getPatrolVerificationPresentation({ runtimeState: 'unavailable' })).toEqual({
title: 'Patrol unavailable',
description: 'Patrol is not ready yet. Check Provider & Models and runtime availability.',
compactLabel: 'Patrol unavailable',
tone: 'error',
});
it('is unproven when the clean full run checked zero resources', () => {
expect(getPatrolRunCoverage([successfulFullRun(0)], NOW)).toBe('unproven');
});
it('is incomplete when the full run ended with errors after checking resources', () => {
expect(
getPatrolRunCoverage(
[makeRun({ resources_checked: 30, error_count: 2, status: 'error' })],
NOW,
),
).toBe('incomplete');
});
it('is incomplete when the full run ended with errors and checked nothing', () => {
expect(
getPatrolRunCoverage(
[makeRun({ resources_checked: 0, error_count: 1, status: 'error' })],
NOW,
),
).toBe('incomplete');
});
it('detects errors via status "error" even when error_count is 0', () => {
expect(
getPatrolRunCoverage(
[makeRun({ resources_checked: 30, error_count: 0, status: 'error' })],
NOW,
),
).toBe('incomplete');
});
it('detects errors case-insensitively via status "ERROR" as wrong-typed input', () => {
const run = makeRun({ resources_checked: 30, error_count: 0 });
(run as unknown as { status: string }).status = ' ERROR ';
expect(getPatrolRunCoverage([run], NOW)).toBe('incomplete');
});
it('reads only the latest completed full run when an older one errored', () => {
expect(
getPatrolRunCoverage(
[
successfulFullRun(40),
makeRun({ id: 'run-older', resources_checked: 40, error_count: 3, status: 'error' }),
],
NOW,
),
).toBe('complete');
});
it('reads only the latest completed full run when an older one was clean', () => {
expect(
getPatrolRunCoverage(
[
makeRun({ id: 'run-newer', resources_checked: 40, error_count: 1, status: 'error' }),
successfulFullRun(40),
],
NOW,
),
).toBe('incomplete');
});
it('skips a full run still in progress', () => {
expect(
getPatrolRunCoverage(
[
makeRun({ id: 'run-in-progress', completed_at: ' ', error_count: 1, status: 'error' }),
successfulFullRun(40),
],
NOW,
),
).toBe('complete');
});
it('lets later clean targeted runs leave a clean full run complete', () => {
expect(
getPatrolRunCoverage(
[
makeRun({ id: 'run-scoped', type: 'scoped', resources_checked: 1 }),
successfulFullRun(40),
],
NOW,
),
).toBe('complete');
});
});
describe('getPatrolVerificationPresentation — full run with errors (hasRunErrors)', () => {
it('reports a needs-review check when the full run has errors and covered resources', () => {
describe('getPatrolRunCoverage — what a clean full run can vouch for', () => {
it('is incomplete when a targeted run after the clean full run failed', () => {
expect(
getPatrolVerificationPresentation({
runs: [
getPatrolRunCoverage(
[
makeRun({
type: 'patrol',
resources_checked: 10,
error_count: 2,
status: 'error',
}),
],
}),
).toEqual({
title: 'Patrol check needs review',
description: 'The most recent Patrol check covered 10 resources but ended with 2 errors.',
compactLabel: 'Check needs review',
tone: 'warning',
lastFullRunAt: '2026-07-10T09:05:00Z',
});
});
it('reports a needs-review check with a generic message when errors occurred and zero resources', () => {
expect(
getPatrolVerificationPresentation({
runs: [
makeRun({
type: 'patrol',
id: 'run-scoped-failed',
type: 'scoped',
started_at: '2026-07-10T09:08:00Z',
completed_at: '2026-07-10T09:09:00Z',
resources_checked: 0,
error_count: 1,
status: 'error',
}),
successfulFullRun(40),
],
}),
).toEqual({
title: 'Patrol check needs review',
description: 'The most recent Patrol check ended with errors.',
compactLabel: 'Check needs review',
tone: 'warning',
lastFullRunAt: '2026-07-10T09:05:00Z',
});
NOW,
),
).toBe('incomplete');
});
it('detects errors via status "error" even when error_count is 0', () => {
const result = getPatrolVerificationPresentation({
runs: [
makeRun({
type: 'patrol',
resources_checked: 5,
error_count: 0,
status: 'error',
}),
],
});
expect(result.title).toBe('Patrol check needs review');
});
it('detects errors case-insensitively via status "ERROR" as wrong-typed input', () => {
const run = makeRun({
type: 'patrol',
resources_checked: 5,
error_count: 0,
status: 'ERROR' as unknown as PatrolRunRecord['status'],
});
expect(getPatrolVerificationPresentation({ runs: [run] }).title).toBe(
'Patrol check needs review',
);
});
});
describe('getPatrolVerificationPresentation — successful full run', () => {
it('reports a successful check with zero resources', () => {
it('is complete when the failure came before the clean full run', () => {
expect(
getPatrolVerificationPresentation({
runs: [
makeRun({
type: 'patrol',
resources_checked: 0,
error_count: 0,
status: 'healthy',
}),
],
}),
).toEqual({
title: 'Recently checked',
description: 'The most recent Patrol check completed successfully.',
compactLabel: 'Recently checked',
tone: 'success',
lastFullRunAt: '2026-07-10T09:05:00Z',
});
});
it('reports a successful check with a single resource (singular)', () => {
expect(
getPatrolVerificationPresentation({
runs: [
makeRun({
type: 'patrol',
resources_checked: 1,
error_count: 0,
status: 'healthy',
}),
],
}).description,
).toBe('The most recent Patrol check completed successfully and covered 1 resource.');
});
});
describe('getPatrolVerificationPresentation — limited runs', () => {
it('reports follow-up checks with zero resources', () => {
expect(
getPatrolVerificationPresentation({
runs: [
makeRun({
type: 'verification',
resources_checked: 0,
error_count: 0,
status: 'healthy',
}),
],
}).description,
).toBe(
'Recent follow-up checks did not cover your full infrastructure. Run Patrol to check everything.',
);
});
it('reports targeted checks with zero resources', () => {
expect(
getPatrolVerificationPresentation({
runs: [
getPatrolRunCoverage(
[
successfulFullRun(40),
makeRun({
id: 'run-scoped-failed',
type: 'scoped',
resources_checked: 0,
error_count: 0,
status: 'healthy',
started_at: '2026-07-10T08:00:00Z',
completed_at: '2026-07-10T08:01:00Z',
error_count: 1,
status: 'error',
}),
],
}).description,
).toBe(
'Recent targeted checks did not cover your full infrastructure. Run Patrol to check everything.',
);
NOW,
),
).toBe('complete');
});
it('reports an unknown-type limited run with resources using the targeted fallback', () => {
it('is unproven when the clean full run finished more than 24 hours ago', () => {
expect(
getPatrolVerificationPresentation({
runs: [
makeRun({
type: 'custom',
resources_checked: 3,
error_count: 0,
status: 'healthy',
}),
],
}).description,
).toBe('Recent targeted checks covered 3 resources. Run Patrol to check everything.');
getPatrolRunCoverage(
[successfulFullRun(40)],
Date.parse('2026-07-10T09:05:00Z') + 24 * 60 * 60 * 1000 + 1,
),
).toBe('unproven');
});
it('uses the default limited description for an unknown type with zero resources', () => {
it('is complete when the clean full run finished exactly 24 hours ago', () => {
expect(
getPatrolVerificationPresentation({
runs: [
makeRun({
type: 'custom',
resources_checked: 0,
error_count: 0,
status: 'healthy',
}),
],
}).description,
).toBe(
'Recent activity only checked part of your infrastructure. Run Patrol to check everything.',
);
getPatrolRunCoverage(
[successfulFullRun(40)],
Date.parse('2026-07-10T09:05:00Z') + 24 * 60 * 60 * 1000,
),
).toBe('complete');
});
it('is unproven when the clean full run has an unparseable completion time', () => {
expect(
getPatrolRunCoverage([makeRun({ resources_checked: 40, completed_at: 'soon' })], NOW),
).toBe('unproven');
});
});
describe('getPatrolVerificationPresentation — no completed runs', () => {
it('reports a pending check when no runs exist', () => {
expect(getPatrolVerificationPresentation({})).toEqual({
title: 'Run Patrol to check',
description: 'Patrol has not completed a check yet.',
compactLabel: 'Check pending',
tone: 'info',
});
describe('getPatrolRunCoverage — no completed full run', () => {
it('is incomplete when only targeted runs completed', () => {
expect(getPatrolRunCoverage([makeRun({ type: 'scoped', resources_checked: 1 })], NOW)).toBe(
'incomplete',
);
});
it('reports a pending check when the only run is not completed', () => {
it('is incomplete when only follow-up checks completed', () => {
expect(
getPatrolVerificationPresentation({
runs: [makeRun({ completed_at: '' })],
}),
).toEqual({
title: 'Run Patrol to check',
description: 'Patrol has not completed a check yet.',
compactLabel: 'Check pending',
tone: 'info',
});
});
});
describe('getVerificationActivityMixLabel (via getPatrolVerificationPresentation)', () => {
it('omits activityMixLabel when there is only a single completed run', () => {
const result = getPatrolVerificationPresentation({
runs: [successfulFullRun(10)],
});
expect(result.activityMixLabel).toBeUndefined();
getPatrolRunCoverage([makeRun({ type: 'verification', resources_checked: 1 })], NOW),
).toBe('incomplete');
});
it('omits activityMixLabel when all completed runs are full patrols', () => {
const result = getPatrolVerificationPresentation({
runs: [
makeRun({
id: 'run-a',
started_at: '2026-07-10T10:00:00Z',
completed_at: '2026-07-10T10:05:00Z',
type: 'patrol',
resources_checked: 40,
}),
makeRun({
id: 'run-b',
started_at: '2026-07-10T09:00:00Z',
completed_at: '2026-07-10T09:05:00Z',
type: 'full',
resources_checked: 40,
}),
],
});
expect(result.activityMixLabel).toBeUndefined();
it('is unproven when no runs exist', () => {
expect(getPatrolRunCoverage(undefined, NOW)).toBe('unproven');
expect(getPatrolRunCoverage([], NOW)).toBe('unproven');
});
it('is unproven when the only run is not completed', () => {
expect(getPatrolRunCoverage([makeRun({ completed_at: undefined })], NOW)).toBe('unproven');
});
});
@@ -458,54 +382,38 @@ describe('getPatrolRecencyPresentation — resourcesCheckedLabel', () => {
// ===========================================================================
// normalizeRunType (private) — exercised through run-type classification in
// verification and recency presentations. Covers case/whitespace/empty
// normalization and the resulting full/scoped/verification classification.
// the run-history coverage verdict. Covers case/whitespace/empty
// normalization and the resulting full/limited classification.
// ===========================================================================
describe('normalizeRunType (via run-type classification)', () => {
it('treats an empty type as a full run', () => {
expect(
getPatrolVerificationPresentation({
runs: [makeRun({ type: '', resources_checked: 5, error_count: 0 })],
}).title,
).toBe('Recently checked');
expect(getPatrolRunCoverage([makeRun({ type: '', resources_checked: 5 })], NOW)).toBe(
'complete',
);
});
it('treats undefined type as a full run', () => {
const run = makeRun({ resources_checked: 5, error_count: 0 });
const run = makeRun({ resources_checked: 5 });
delete (run as Partial<PatrolRunRecord>).type;
expect(getPatrolVerificationPresentation({ runs: [run] }).title).toBe('Recently checked');
expect(getPatrolRunCoverage([run], NOW)).toBe('complete');
});
it('treats "PATROL" (uppercase) as a full run', () => {
expect(
getPatrolVerificationPresentation({
runs: [makeRun({ type: 'PATROL', resources_checked: 5, error_count: 0 })],
}).title,
).toBe('Recently checked');
expect(getPatrolRunCoverage([makeRun({ type: 'PATROL', resources_checked: 5 })], NOW)).toBe(
'complete',
);
});
it('treats " Full " (whitespace, mixed case) as a full run', () => {
expect(
getPatrolVerificationPresentation({
runs: [makeRun({ type: ' Full ', resources_checked: 5, error_count: 0 })],
}).title,
).toBe('Recently checked');
expect(getPatrolRunCoverage([makeRun({ type: ' Full ', resources_checked: 5 })], NOW)).toBe(
'complete',
);
});
it('classifies "SCOPED" (uppercase) as a scoped run', () => {
expect(
getPatrolVerificationPresentation({
runs: [makeRun({ type: 'SCOPED', resources_checked: 1, error_count: 0 })],
}).description,
).toContain('targeted checks');
});
it('classifies "verification" as a verification run', () => {
expect(
getPatrolVerificationPresentation({
runs: [makeRun({ type: 'verification', resources_checked: 1, error_count: 0 })],
}).description,
).toContain('follow-up checks');
it('classifies "SCOPED" (uppercase) as a limited run', () => {
expect(getPatrolRunCoverage([makeRun({ type: 'SCOPED', resources_checked: 1 })], NOW)).toBe(
'incomplete',
);
});
});
@@ -1,14 +1,17 @@
import { describe, expect, it } from 'vitest';
import {
getPatrolRecencyPresentation,
getPatrolVerificationPresentation,
getPatrolRunCoverage,
} from '@/utils/patrolSummaryPresentation';
describe('Patrol recency and verification presentation', () => {
it('reports a recent successful Patrol check', () => {
// Half an hour after the fixture runs below.
const NOW = Date.parse('2026-03-12T10:30:00Z');
describe('Patrol recency and run-history coverage', () => {
it('proves coverage with a recent successful Patrol check', () => {
expect(
getPatrolVerificationPresentation({
runs: [
getPatrolRunCoverage(
[
{
id: 'run-1',
started_at: '2026-03-12T09:50:00Z',
@@ -38,20 +41,15 @@ describe('Patrol recency and verification presentation', () => {
tool_call_count: 0,
},
] as never,
}),
).toEqual({
title: 'Recently checked',
description: 'The most recent Patrol check completed successfully and covered 58 resources.',
compactLabel: 'Recently checked',
tone: 'success',
lastFullRunAt: '2026-03-12T09:57:00Z',
});
NOW,
),
).toBe('complete');
});
it('adds a check mix when targeted runs make recent activity look busy', () => {
it('keeps coverage proven when targeted runs follow a clean full check', () => {
expect(
getPatrolVerificationPresentation({
runs: [
getPatrolRunCoverage(
[
{
id: 'run-scoped-alert',
started_at: '2026-03-12T10:00:00Z',
@@ -139,21 +137,15 @@ describe('Patrol recency and verification presentation', () => {
tool_call_count: 0,
},
] as never,
}),
).toEqual({
title: 'Recently checked',
description: 'The most recent Patrol check completed successfully and covered 58 resources.',
compactLabel: 'Recently checked',
tone: 'success',
lastFullRunAt: '2026-03-12T09:57:00Z',
activityMixLabel: '1 full check, 1 alert-triggered check, 1 anomaly-triggered check',
});
NOW,
),
).toBe('complete');
});
it('reports a partial check when only targeted runs are recent', () => {
it('reports incomplete coverage when only targeted runs are recent', () => {
expect(
getPatrolVerificationPresentation({
runs: [
getPatrolRunCoverage(
[
{
id: 'run-1',
started_at: '2026-03-12T09:58:00Z',
@@ -184,19 +176,15 @@ describe('Patrol recency and verification presentation', () => {
tool_call_count: 0,
},
] as never,
}),
).toEqual({
title: 'Needs full check',
description: 'Recent targeted checks covered 1 resource. Run Patrol to check everything.',
compactLabel: 'Partial check',
tone: 'warning',
});
NOW,
),
).toBe('incomplete');
});
it('reports a partial check when only follow-up checks are recent', () => {
it('reports incomplete coverage when only follow-up checks are recent', () => {
expect(
getPatrolVerificationPresentation({
runs: [
getPatrolRunCoverage(
[
{
id: 'run-1',
started_at: '2026-03-12T09:58:00Z',
@@ -227,13 +215,9 @@ describe('Patrol recency and verification presentation', () => {
tool_call_count: 0,
},
] as never,
}),
).toEqual({
title: 'Needs full check',
description: 'Recent follow-up checks covered 1 resource. Run Patrol to check everything.',
compactLabel: 'Partial check',
tone: 'warning',
});
NOW,
),
).toBe('incomplete');
});
it('labels targeted recency as the last check', () => {
@@ -5,7 +5,8 @@ import { getPatrolRuntimePresentation } from '@/utils/patrolRuntimePresentation'
import type { SemanticTone } from '@/utils/semanticTonePresentation';
import type { IntelligenceHealthScore } from '@/types/aiIntelligence';
import { getPatrolRunCoverageSummary, isPatrolRunHealthy } from '@/utils/patrolRunPresentation';
import { getPatrolVerificationPresentation } from '@/utils/patrolSummaryPresentation';
import type { PatrolRunCoverage } from '@/utils/patrolSummaryPresentation';
import { getPatrolRunCoverage } from '@/utils/patrolSummaryPresentation';
export function getInvestigationMessagesState(loading: boolean, hasMessages: boolean) {
if (loading) {
@@ -86,53 +87,41 @@ function getHealthDegradedTone(overallHealth: IntelligenceHealthScore): Semantic
return overallHealth.grade === 'D' || overallHealth.grade === 'F' ? 'error' : 'warning';
}
function hasCoverageFactor(overallHealth: IntelligenceHealthScore | undefined): boolean {
return Boolean(overallHealth?.factors.some((factor) => factor.category === 'coverage'));
}
// Run history decides coverage before the health summary does. The summary
// keeps its `Recent Patrol errors` coverage factor until three clean full runs
// lead its window or the errored runs leave it (summarizeRecentPatrolCoverage
// in internal/ai/intelligence.go), so a clean full run that supersedes those
// errors must outrank that factor, or the queue keeps asking for a check after
// Patrol has already checked everything.
function hasIncompleteCoverageEvidence(
overallHealth: IntelligenceHealthScore | undefined,
runs: PatrolRunRecord[] | undefined,
runCoverage: PatrolRunCoverage,
): boolean {
if (overallHealth?.factors.some((factor) => factor.category === 'coverage')) {
return true;
if (runCoverage !== 'unproven') {
return runCoverage === 'incomplete';
}
const verification = getPatrolVerificationPresentation({ runs });
return verification.tone === 'warning';
return hasCoverageFactor(overallHealth);
}
function shouldSuppressHealthyEmptyState(
overallHealth: IntelligenceHealthScore | undefined,
runs: PatrolRunRecord[] | undefined,
// Once run history proves coverage, a grade that only the coverage factor
// lowered is not degraded health.
function hasDegradedHealth(
overallHealth: IntelligenceHealthScore,
runCoverage: PatrolRunCoverage,
): boolean {
if (hasIncompleteCoverageEvidence(overallHealth, runs)) {
return true;
}
if (!overallHealth) {
if (overallHealth.grade === 'A') {
return false;
}
return overallHealth.grade !== 'A';
}
function getDegradedPatrolEmptyStateBody(
overallHealth: IntelligenceHealthScore | undefined,
runs: PatrolRunRecord[] | undefined,
): string {
if (hasIncompleteCoverageEvidence(overallHealth, runs)) {
return DEGRADED_COVERAGE_EMPTY_STATE_BODY;
if (runCoverage !== 'complete') {
return true;
}
return DEGRADED_HEALTH_EMPTY_STATE_BODY;
}
function getDegradedPatrolEmptyStateTitle(
overallHealth: IntelligenceHealthScore | undefined,
runs: PatrolRunRecord[] | undefined,
): string {
if (hasIncompleteCoverageEvidence(overallHealth, runs)) {
return PATROL_QUEUE_RECHECK_TITLE;
}
return PATROL_QUEUE_REVIEW_TITLE;
return overallHealth.factors.some(
(factor) => factor.category !== 'coverage' && factor.impact < 0,
);
}
function hasHistoricalRegressions(count: number | undefined): boolean {
@@ -189,6 +178,7 @@ export function getPatrolFindingsEmptyState(args: {
coverageStale?: boolean;
runs?: PatrolRunRecord[];
runSnapshot?: PatrolRunSnapshotEmptyStateArgs;
nowMs?: number;
}): PatrolFindingsEmptyStateCopy {
if (
args.filter === 'all' &&
@@ -239,14 +229,23 @@ export function getPatrolFindingsEmptyState(args: {
};
}
if (shouldSuppressHealthyEmptyState(args.overallHealth, args.runs)) {
const runCoverage = getPatrolRunCoverage(args.runs, args.nowMs);
if (hasIncompleteCoverageEvidence(args.overallHealth, runCoverage)) {
return {
title: getDegradedPatrolEmptyStateTitle(args.overallHealth, args.runs),
body: getDegradedPatrolEmptyStateBody(args.overallHealth, args.runs),
title: PATROL_QUEUE_RECHECK_TITLE,
body: DEGRADED_COVERAGE_EMPTY_STATE_BODY,
tone: args.overallHealth ? getHealthDegradedTone(args.overallHealth) : 'warning',
};
}
if (args.overallHealth && hasDegradedHealth(args.overallHealth, runCoverage)) {
return {
title: PATROL_QUEUE_REVIEW_TITLE,
body: DEGRADED_HEALTH_EMPTY_STATE_BODY,
tone: getHealthDegradedTone(args.overallHealth),
};
}
if (hasHistoricalRegressions(args.historicalRegressionCount)) {
return {
title: PATROL_QUEUE_CLEAR_TITLE,
@@ -37,17 +37,6 @@ export interface PatrolRunOperatorRecordPresentation {
headline: string;
}
export interface PatrolActivityBreakdown {
totalRuns: number;
fullPatrols: number;
verificationChecks: number;
alertTriggeredRuns: number;
anomalyTriggeredRuns: number;
alertClearedRuns: number;
otherScopedRuns: number;
newFindings: number;
}
export type PatrolRunPrimaryActionPresentation = PatrolRuntimeActionPresentation;
export const PATROL_FINDING_RECORD_UNAVAILABLE_LABEL = 'Finding record unavailable';
@@ -437,114 +426,6 @@ export function getPatrolTriggerStatusSummary(
return notes.length > 0 ? notes.join(' · ') : undefined;
}
function isSameLocalDay(timestamp: string | undefined, referenceDate: Date): boolean {
if (!timestamp) return false;
const value = new Date(timestamp);
if (Number.isNaN(value.getTime())) return false;
return (
value.getFullYear() === referenceDate.getFullYear() &&
value.getMonth() === referenceDate.getMonth() &&
value.getDate() === referenceDate.getDate()
);
}
export function getPatrolActivityBreakdown(
runs: PatrolRunRecord[],
referenceDate: Date = new Date(),
): PatrolActivityBreakdown {
return runs.reduce<PatrolActivityBreakdown>(
(summary, run) => {
if (!isSameLocalDay(run.started_at, referenceDate)) {
return summary;
}
summary.totalRuns += 1;
summary.newFindings += Math.max(0, run.new_findings || 0);
if (isFullPatrolRunType(run.type)) {
summary.fullPatrols += 1;
return summary;
}
switch (normalizePatrolRunType(run.type)) {
case 'verification':
summary.verificationChecks += 1;
return summary;
}
switch (normalizePatrolRunType(run.trigger_reason)) {
case 'alert_fired':
summary.alertTriggeredRuns += 1;
break;
case 'anomaly':
summary.anomalyTriggeredRuns += 1;
break;
case 'alert_cleared':
summary.alertClearedRuns += 1;
break;
default:
summary.otherScopedRuns += 1;
break;
}
return summary;
},
{
totalRuns: 0,
fullPatrols: 0,
verificationChecks: 0,
alertTriggeredRuns: 0,
anomalyTriggeredRuns: 0,
alertClearedRuns: 0,
otherScopedRuns: 0,
newFindings: 0,
},
);
}
function formatCountLabel(count: number, singular: string, plural?: string): string {
return `${count} ${count === 1 ? singular : (plural ?? `${singular}s`)}`;
}
export function formatPatrolActivityBreakdown(summary: PatrolActivityBreakdown): string {
const segments: string[] = [];
if (summary.fullPatrols > 0) {
segments.push(formatCountLabel(summary.fullPatrols, 'full check', 'full checks'));
}
if (summary.alertTriggeredRuns > 0) {
segments.push(
formatCountLabel(
summary.alertTriggeredRuns,
'alert-triggered check',
'alert-triggered checks',
),
);
}
if (summary.anomalyTriggeredRuns > 0) {
segments.push(
formatCountLabel(
summary.anomalyTriggeredRuns,
'anomaly-triggered check',
'anomaly-triggered checks',
),
);
}
if (summary.alertClearedRuns > 0) {
segments.push(
formatCountLabel(summary.alertClearedRuns, 'alert-cleared check', 'alert-cleared checks'),
);
}
if (summary.verificationChecks > 0) {
segments.push(
formatCountLabel(summary.verificationChecks, 'follow-up check', 'follow-up checks'),
);
}
if (summary.otherScopedRuns > 0) {
segments.push(formatCountLabel(summary.otherScopedRuns, 'targeted check', 'targeted checks'));
}
return segments.join(', ');
}
export function getPatrolRunCoverageSummary(
run: Pick<
PatrolRunRecord,
@@ -1,19 +1,17 @@
import type { PatrolRunRecord, PatrolRuntimeState } from '@/api/patrol';
import {
formatPatrolActivityBreakdown,
getPatrolActivityBreakdown,
} from '@/utils/patrolRunPresentation';
import type { SemanticTone } from '@/utils/semanticTonePresentation';
import { getPatrolRuntimePresentation } from '@/utils/patrolRuntimePresentation';
import type { PatrolRunRecord } from '@/api/patrol';
export interface PatrolVerificationPresentation {
title: string;
description: string;
compactLabel: string;
tone: SemanticTone;
lastFullRunAt?: string;
activityMixLabel?: string;
}
// What Patrol run history proves about coverage of the whole estate.
// `complete`: the latest completed full patrol ended without errors, checked
// at least one resource, finished within the last 24 hours, and no run after
// it failed. `incomplete`: it ended with errors, a run after it failed, or
// only targeted or follow-up runs have completed. `unproven`: anything else,
// including no completed run.
export type PatrolRunCoverage = 'complete' | 'incomplete' | 'unproven';
// The backend judges coverage over the same 24 hours
// (intelligencePatrolCoverageWindow in internal/ai/intelligence.go), so an
// older clean run cannot vouch for what its coverage factor reports.
const PATROL_RUN_COVERAGE_PROOF_WINDOW_MS = 24 * 60 * 60 * 1000;
export interface PatrolRecencyPresentation {
label: string;
@@ -38,36 +36,6 @@ function isFullPatrolRun(run: PatrolRunRecord): boolean {
return normalized === '' || normalized === 'full' || normalized === 'patrol';
}
function isScopedPatrolRun(run: PatrolRunRecord): boolean {
return normalizeRunType(run.type) === 'scoped';
}
function isVerificationPatrolRun(run: PatrolRunRecord): boolean {
return normalizeRunType(run.type) === 'verification';
}
function getVerificationActivityMixLabel(runs: PatrolRunRecord[]): string | undefined {
const latestCompletedRun = runs.find((run) => isCompletedPatrolRun(run));
const referenceTimestamp = latestCompletedRun?.completed_at || latestCompletedRun?.started_at;
if (!referenceTimestamp) {
return undefined;
}
const breakdown = getPatrolActivityBreakdown(runs, new Date(referenceTimestamp));
const scopedRuns =
breakdown.alertTriggeredRuns +
breakdown.anomalyTriggeredRuns +
breakdown.alertClearedRuns +
breakdown.verificationChecks +
breakdown.otherScopedRuns;
if (breakdown.totalRuns <= 1 || scopedRuns <= 0) {
return undefined;
}
const label = formatPatrolActivityBreakdown(breakdown);
return label || undefined;
}
function isCompletedPatrolRun(run: PatrolRunRecord): boolean {
return Boolean(run.completed_at?.trim());
}
@@ -90,93 +58,29 @@ function formatRecencyResourcesCheckedLabel(run: PatrolRunRecord): string | unde
return `checked ${resourcesChecked} resource${resourcesChecked === 1 ? '' : 's'}`;
}
export function getPatrolVerificationPresentation(args: {
runs?: PatrolRunRecord[];
runtimeState?: PatrolRuntimeState;
blockedReason?: string;
}): PatrolVerificationPresentation {
if (
args.runtimeState === 'blocked' ||
args.runtimeState === 'disabled' ||
args.runtimeState === 'unavailable'
) {
const runtime = getPatrolRuntimePresentation(args.runtimeState, args.blockedReason);
return {
title: runtime.label,
description: runtime.description,
compactLabel: runtime.label,
tone: runtime.tone,
};
export function getPatrolRunCoverage(
runs: PatrolRunRecord[] | undefined,
nowMs: number = Date.now(),
): PatrolRunCoverage {
const completedRuns = (runs ?? []).filter((run) => isCompletedPatrolRun(run));
const latestFullRunIndex = completedRuns.findIndex((run) => isFullPatrolRun(run));
if (latestFullRunIndex < 0) {
return completedRuns.length > 0 ? 'incomplete' : 'unproven';
}
const completedRuns = (args.runs ?? []).filter((run) => isCompletedPatrolRun(run));
const activityMixLabel = getVerificationActivityMixLabel(completedRuns);
const recentFullRun = completedRuns.find((run) => isFullPatrolRun(run));
if (recentFullRun) {
const resourcesChecked = recentFullRun.resources_checked || 0;
if (hasRunErrors(recentFullRun)) {
return {
title: 'Patrol check needs review',
description:
resourcesChecked > 0
? `The most recent Patrol check covered ${resourcesChecked} resource${resourcesChecked === 1 ? '' : 's'} but ended with ${recentFullRun.error_count} error${recentFullRun.error_count === 1 ? '' : 's'}.`
: 'The most recent Patrol check ended with errors.',
compactLabel: 'Check needs review',
tone: 'warning',
lastFullRunAt: recentFullRun.completed_at,
activityMixLabel,
};
}
return {
title: 'Recently checked',
description:
resourcesChecked > 0
? `The most recent Patrol check completed successfully and covered ${resourcesChecked} resource${resourcesChecked === 1 ? '' : 's'}.`
: 'The most recent Patrol check completed successfully.',
compactLabel: 'Recently checked',
tone: 'success',
lastFullRunAt: recentFullRun.completed_at,
activityMixLabel,
};
const latestFullRun = completedRuns[latestFullRunIndex];
// A clean full run supersedes only the failures before it, not one after.
if (completedRuns.slice(0, latestFullRunIndex + 1).some((run) => hasRunErrors(run))) {
return 'incomplete';
}
const recentLimitedRun = completedRuns.find((run) => !isFullPatrolRun(run));
if (recentLimitedRun) {
const resourcesChecked = recentLimitedRun.resources_checked || 0;
let description =
'Recent activity only checked part of your infrastructure. Run Patrol to check everything.';
if (isVerificationPatrolRun(recentLimitedRun)) {
description =
resourcesChecked > 0
? `Recent follow-up checks covered ${resourcesChecked} resource${resourcesChecked === 1 ? '' : 's'}. Run Patrol to check everything.`
: 'Recent follow-up checks did not cover your full infrastructure. Run Patrol to check everything.';
} else if (isScopedPatrolRun(recentLimitedRun)) {
description =
resourcesChecked > 0
? `Recent targeted checks covered ${resourcesChecked} resource${resourcesChecked === 1 ? '' : 's'}. Run Patrol to check everything.`
: 'Recent targeted checks did not cover your full infrastructure. Run Patrol to check everything.';
} else if (resourcesChecked > 0) {
description = `Recent targeted checks covered ${resourcesChecked} resource${resourcesChecked === 1 ? '' : 's'}. Run Patrol to check everything.`;
}
return {
title: 'Needs full check',
description,
compactLabel: 'Partial check',
tone: 'warning',
activityMixLabel,
};
if ((latestFullRun.resources_checked || 0) <= 0) {
return 'unproven';
}
return {
title: 'Run Patrol to check',
description: 'Patrol has not completed a check yet.',
compactLabel: 'Check pending',
tone: 'info',
};
const completedMs = Date.parse(latestFullRun.completed_at);
if (!Number.isFinite(completedMs) || nowMs - completedMs > PATROL_RUN_COVERAGE_PROOF_WINDOW_MS) {
return 'unproven';
}
return 'complete';
}
export function getPatrolRecencyPresentation(args: {