mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-21 10:43:36 +00:00
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:
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user