Separate Patrol runtime issues from infrastructure issues

Patrol assessment now classifies synthetic Patrol runtime findings separately from infrastructure findings so the top summary can truthfully describe Patrol-owned service failures.
This commit is contained in:
rcourtman
2026-03-25 17:24:20 +00:00
parent 860f856a35
commit d5d61891ed
6 changed files with 174 additions and 7 deletions
@@ -142,6 +142,12 @@ must not pair an `Issues detected` headline with a raw coverage-only
`overall_health.prediction` sentence from a separate source; when active
findings and incomplete verification are both true, the Patrol summary should
describe both in one canonical assessment message.
That same assessment contract must also distinguish Patrol-owned runtime
findings from infrastructure findings. When the only active Patrol findings are
synthetic Patrol service/runtime conditions such as the `ai-service`
provider-credit failure, the top assessment should read as a Patrol runtime
issue rather than implying infrastructure issues were detected across the
estate.
The summary recency chip must follow the same governed scope distinction. When
the latest completed activity was only a scoped run, the summary should label
that timestamp as `Last activity` instead of `Last patrol`; `Last full patrol`
@@ -46,6 +46,7 @@ export function PatrolIntelligenceSummary(props: { state: PatrolIntelligenceStat
blockedReason: state.blockedReason(),
criticalFindings: summaryStats().criticalFindings,
warningFindings: summaryStats().warningFindings,
activeFindings: state.activePatrolFindings(),
}),
);
const assessmentTonePresentation = createMemo(() =>
@@ -605,6 +605,15 @@ export function usePatrolIntelligenceState() {
};
};
const activePatrolFindings = () =>
aiIntelligenceStore.findings.filter(
(finding) =>
finding.status === 'active' &&
finding.source !== 'threshold' &&
!finding.isThreshold &&
!hasTriggeringAlert(finding),
);
onMount(async () => {
await Promise.all([
loadLicenseStatus(),
@@ -644,6 +653,7 @@ export function usePatrolIntelligenceState() {
return {
activeTab,
activePatrolFindings,
activityRefreshTrigger,
alertAnalysisUpgradeUrl,
alertAnalysisLocked,
@@ -889,12 +889,14 @@ describe('AIIntelligence entitlement gating', () => {
intelligenceState.findings = [
{
id: 'finding-1',
source: 'patrol',
source: 'ai-patrol',
isThreshold: false,
status: 'active',
severity: 'warning',
resourceId: 'svc-patrol',
resourceId: 'ai-service',
resourceName: 'Pulse Patrol Service',
resourceType: 'service',
title: 'Pulse Patrol: Insufficient API credits',
detectedAt: '2026-03-12T09:57:00Z',
},
];
@@ -938,10 +940,10 @@ describe('AIIntelligence entitlement gating', () => {
render(() => <AIIntelligence />);
await waitFor(() => {
expect(screen.getByText('Issues detected')).toBeInTheDocument();
expect(screen.getByText('Patrol runtime issue')).toBeInTheDocument();
expect(
screen.getByText(
'Patrol surfaced 1 active warning finding. Recent coverage is also incomplete, so the rest of your infrastructure is not fully verified.',
'Patrol surfaced 1 active warning finding about its own runtime. Recent coverage is also incomplete, so the rest of your infrastructure is not fully verified.',
),
).toBeInTheDocument();
});
@@ -107,6 +107,45 @@ describe('getPatrolSummaryPresentation', () => {
});
});
it('classifies patrol-owned service failures as runtime issues instead of infrastructure issues', () => {
expect(
getPatrolAssessmentPresentation({
overallHealth: {
score: 60,
grade: 'C',
trend: 'stable',
factors: [
{
name: 'Patrol coverage incomplete',
impact: -0.35,
description: 'Patrol coverage is incomplete.',
category: 'coverage',
},
],
prediction:
'Patrol coverage is incomplete: recent activity was limited to scoped runs and ended with errors, so overall health is not fully verified.',
},
warningFindings: 1,
activeFindings: [
{
status: 'active',
severity: 'warning',
resourceId: 'ai-service',
resourceName: 'Pulse Patrol Service',
title: 'Pulse Patrol: Insufficient API credits',
},
] as never,
}),
).toEqual({
title: 'Patrol runtime issue',
description:
'Patrol surfaced 1 active warning finding about its own runtime. Recent coverage is also incomplete, so the rest of your infrastructure is not fully verified.',
eyebrow: 'Patrol assessment',
compactLabel: 'Patrol runtime issue',
tone: 'warning',
});
});
it('keeps no-issues copy only for fully healthy patrol states', () => {
expect(
getPatrolNoIssuesPresentation({
@@ -1,4 +1,5 @@
import type { PatrolRunRecord, PatrolRuntimeState } from '@/api/patrol';
import type { UnifiedFinding } from '@/stores/aiIntelligence';
import type { IntelligenceHealthScore } from '@/types/aiIntelligence';
import type { SemanticTone } from '@/utils/semanticTonePresentation';
import { getPatrolRuntimePresentation } from '@/utils/patrolRuntimePresentation';
@@ -37,6 +38,11 @@ export interface PatrolRecencyPresentation {
timestamp?: string;
}
type PatrolAssessmentFinding = Pick<
UnifiedFinding,
'resourceId' | 'resourceName' | 'title' | 'severity' | 'status'
>;
export const PATROL_NO_ISSUES_LABEL = 'No issues found';
const QUIET_ICON_CONTAINER = 'bg-surface border-border';
@@ -90,21 +96,101 @@ function formatFindingCount(count: number, severity: 'critical' | 'warning'): st
return `${count} active ${severity} finding${count === 1 ? '' : 's'}`;
}
function isPatrolRuntimeFinding(finding: PatrolAssessmentFinding): boolean {
const resourceId = String(finding.resourceId || '').trim().toLowerCase();
const resourceName = String(finding.resourceName || '').trim().toLowerCase();
const title = String(finding.title || '').trim().toLowerCase();
return (
resourceId === 'ai-service' ||
resourceName === 'pulse patrol service' ||
title.startsWith('pulse patrol:')
);
}
function classifyActiveFindings(activeFindings: PatrolAssessmentFinding[] | undefined) {
const runtimeCritical = (activeFindings ?? []).filter(
(finding) =>
finding.status === 'active' &&
finding.severity === 'critical' &&
isPatrolRuntimeFinding(finding),
).length;
const runtimeWarning = (activeFindings ?? []).filter(
(finding) =>
finding.status === 'active' &&
finding.severity === 'warning' &&
isPatrolRuntimeFinding(finding),
).length;
const infrastructureCritical = (activeFindings ?? []).filter(
(finding) =>
finding.status === 'active' &&
finding.severity === 'critical' &&
!isPatrolRuntimeFinding(finding),
).length;
const infrastructureWarning = (activeFindings ?? []).filter(
(finding) =>
finding.status === 'active' &&
finding.severity === 'warning' &&
!isPatrolRuntimeFinding(finding),
).length;
return {
runtimeCritical,
runtimeWarning,
infrastructureCritical,
infrastructureWarning,
runtimeTotal: runtimeCritical + runtimeWarning,
infrastructureTotal: infrastructureCritical + infrastructureWarning,
};
}
function joinAssessmentParts(parts: string[]): string {
if (parts.length <= 1) return parts[0] ?? '';
if (parts.length === 2) return `${parts[0]} and ${parts[1]}`;
return `${parts.slice(0, -1).join(', ')}, and ${parts.at(-1)}`;
}
function getFindingAssessmentDescription(args: {
criticalFindings?: number;
warningFindings?: number;
overallHealth?: IntelligenceHealthScore;
activeFindings?: PatrolAssessmentFinding[];
}): string {
const criticalFindings = args.criticalFindings ?? 0;
const warningFindings = args.warningFindings ?? 0;
const hasCoverageGap = Boolean(
args.overallHealth?.factors.some((factor) => factor.category === 'coverage'),
);
const classified = classifyActiveFindings(args.activeFindings);
const findingSummaryParts: string[] = [];
if (classified.infrastructureCritical > 0) {
findingSummaryParts.push(
`${formatFindingCount(classified.infrastructureCritical, 'critical')} in your infrastructure`,
);
}
if (classified.infrastructureWarning > 0) {
findingSummaryParts.push(
`${formatFindingCount(classified.infrastructureWarning, 'warning')} in your infrastructure`,
);
}
if (classified.runtimeCritical > 0) {
findingSummaryParts.push(
`${formatFindingCount(classified.runtimeCritical, 'critical')} about its own runtime`,
);
}
if (classified.runtimeWarning > 0) {
findingSummaryParts.push(
`${formatFindingCount(classified.runtimeWarning, 'warning')} about its own runtime`,
);
}
const findingSummary =
criticalFindings > 0
? `Patrol surfaced ${formatFindingCount(criticalFindings, 'critical')}.`
: `Patrol surfaced ${formatFindingCount(warningFindings, 'warning')}.`;
findingSummaryParts.length > 0
? `Patrol surfaced ${joinAssessmentParts(findingSummaryParts)}.`
: criticalFindings > 0
? `Patrol surfaced ${formatFindingCount(criticalFindings, 'critical')}.`
: `Patrol surfaced ${formatFindingCount(warningFindings, 'warning')}.`;
if (hasCoverageGap) {
return `${findingSummary} Recent coverage is also incomplete, so the rest of your infrastructure is not fully verified.`;
@@ -141,7 +227,10 @@ export function getPatrolAssessmentPresentation(args: {
blockedReason?: string;
criticalFindings?: number;
warningFindings?: number;
activeFindings?: PatrolAssessmentFinding[];
}): PatrolAssessmentPresentation {
const classified = classifyActiveFindings(args.activeFindings);
if (
args.runtimeState === 'blocked' ||
args.runtimeState === 'disabled' ||
@@ -158,6 +247,16 @@ export function getPatrolAssessmentPresentation(args: {
}
if ((args.criticalFindings ?? 0) > 0) {
if (classified.infrastructureTotal === 0 && classified.runtimeTotal > 0) {
return {
title: 'Critical Patrol runtime issue',
description: getFindingAssessmentDescription(args),
eyebrow: 'Patrol assessment',
compactLabel: 'Patrol runtime issue',
tone: 'error',
};
}
return {
title: 'Critical issues detected',
description: getFindingAssessmentDescription(args),
@@ -168,6 +267,16 @@ export function getPatrolAssessmentPresentation(args: {
}
if ((args.warningFindings ?? 0) > 0) {
if (classified.infrastructureTotal === 0 && classified.runtimeTotal > 0) {
return {
title: 'Patrol runtime issue',
description: getFindingAssessmentDescription(args),
eyebrow: 'Patrol assessment',
compactLabel: 'Patrol runtime issue',
tone: 'warning',
};
}
return {
title: 'Issues detected',
description: getFindingAssessmentDescription(args),