mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-07-10 00:14:38 +00:00
patrol: show actionable state badges on collapsed Patrol finding rows
Patrol findings hide raw investigation-status and investigation-outcome badges per the patrol-intelligence contract, but the contract explicitly allows plain-language actionable state labels (approval required, investigating, verifying fix, fix failed). This adds a getPatrolFinding- ActionableState helper and renders the resulting badge on the collapsed Patrol row between the severity badge and the title, so the operator sees each finding's state without expanding. The badge is absent when no actionable state applies, leaving the default new-finding row clean.
This commit is contained in:
parent
370017b48d
commit
7d0b481e22
5 changed files with 96 additions and 5 deletions
|
|
@ -7336,11 +7336,11 @@
|
|||
{
|
||||
"id": "opencode-coverage-gap-protection-posture-attention-queue",
|
||||
"agent_id": "opencode",
|
||||
"summary": "Patrol workspace work-type grouping: classify findings by actionable type and surface composition in the workspace header",
|
||||
"summary": "Patrol row-level actionable state labels: translate hidden investigation state into plain-language badges on collapsed Patrol finding rows",
|
||||
"target_id": "v6-product-lane-expansion",
|
||||
"claimed_at": "2026-06-25T22:11:19Z",
|
||||
"heartbeat_at": "2026-06-25T22:11:19Z",
|
||||
"expires_at": "2026-06-26T00:11:19Z",
|
||||
"claimed_at": "2026-06-25T22:24:34Z",
|
||||
"heartbeat_at": "2026-06-25T22:24:34Z",
|
||||
"expires_at": "2026-06-26T00:24:34Z",
|
||||
"work_item": {
|
||||
"kind": "coverage-gap",
|
||||
"id": "protection-posture-attention-queue"
|
||||
|
|
|
|||
|
|
@ -80,6 +80,7 @@ import {
|
|||
getInvestigationStatusLabel,
|
||||
getInvestigationOutcomeSortOrder,
|
||||
getInvestigationStatusBadgeTone,
|
||||
getPatrolFindingActionableState,
|
||||
} from '@/utils/aiFindingPresentation';
|
||||
import { copyToClipboard } from '@/utils/clipboard';
|
||||
|
||||
|
|
@ -884,6 +885,7 @@ export const FindingsPanel: Component<FindingsPanelProps> = (props) => {
|
|||
const rowPrimaryAction = getFindingPrimaryActionPresentation(finding);
|
||||
const patrolWorkflow = () =>
|
||||
getFindingPatrolWorkflowPresentation(finding, aiIntelligenceStore.patrolPendingApprovals);
|
||||
const patrolActionableState = () => getPatrolFindingActionableState(finding);
|
||||
const collapsedApprovalAction = () => {
|
||||
const workflow = patrolWorkflow();
|
||||
return workflow?.stage === 'approval' ? workflow : undefined;
|
||||
|
|
@ -976,6 +978,19 @@ export const FindingsPanel: Component<FindingsPanelProps> = (props) => {
|
|||
>
|
||||
{severityPresentation.label}
|
||||
</MetadataBadge>
|
||||
{/* Patrol actionable state — plain-language translation of
|
||||
investigation state that the contract allows on the
|
||||
collapsed Patrol row (approval required, investigating,
|
||||
verifying fix, fix failed). Raw investigation-status and
|
||||
investigation-outcome badges stay hidden for Patrol
|
||||
findings; this badge surfaces the operator-facing meaning. */}
|
||||
<Show when={isPatrolFindingsSource() && patrolActionableState()}>
|
||||
{(state) => (
|
||||
<MetadataBadge {...FINDING_ROW_BADGE_PROPS} tone={state().tone}>
|
||||
{state().label}
|
||||
</MetadataBadge>
|
||||
)}
|
||||
</Show>
|
||||
{/* Alert-triggered badge */}
|
||||
<Show when={hasTriggeringAlert(finding)}>
|
||||
<MetadataBadge
|
||||
|
|
|
|||
|
|
@ -960,6 +960,12 @@ describe('aiFindingPresentation', () => {
|
|||
expect(patrolWorkspaceSource).toContain('workTypeComposition');
|
||||
});
|
||||
|
||||
it('shows contract-sanctioned actionable state badges on collapsed Patrol rows', () => {
|
||||
expect(findingsPanelSource).toContain('getPatrolFindingActionableState');
|
||||
expect(findingsPanelSource).toContain('patrolActionableState');
|
||||
expect(findingsPanelSource).toContain('isPatrolFindingsSource() && patrolActionableState()');
|
||||
});
|
||||
|
||||
it('does not stack a default detected loop-state badge on active findings', () => {
|
||||
expect(findingsPanelSource).toContain('const shouldShowLoopStateBadge = () =>');
|
||||
expect(findingsPanelSource).toContain('!isPatrolFindingsSource()');
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ import {
|
|||
classifyPatrolFindingWorkType,
|
||||
getPatrolWorkTypeComposition,
|
||||
getPatrolWorkTypeCompositionClause,
|
||||
getPatrolFindingActionableState,
|
||||
} from '@/utils/aiFindingPresentation';
|
||||
import type { UnifiedFinding } from '@/stores/aiIntelligence';
|
||||
|
||||
type ClassifyInput = Parameters<typeof classifyPatrolFindingWorkType>[0];
|
||||
|
||||
|
|
@ -178,3 +178,44 @@ describe('getPatrolWorkTypeCompositionClause', () => {
|
|||
).toBe(' — 1 needs approval, 1 failed fix, 2 recurring');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPatrolFindingActionableState', () => {
|
||||
it('returns undefined for a plain new finding', () => {
|
||||
expect(getPatrolFindingActionableState(makeFinding())).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns approval required for fix_queued', () => {
|
||||
expect(
|
||||
getPatrolFindingActionableState(makeFinding({ investigationOutcome: 'fix_queued' })),
|
||||
).toEqual({ label: 'Approval required', tone: 'warning' });
|
||||
});
|
||||
|
||||
it.each(['fix_failed', 'fix_verification_failed', 'cannot_fix', 'timed_out'] as const)(
|
||||
'returns fix failed for %s',
|
||||
(outcome) => {
|
||||
expect(getPatrolFindingActionableState(makeFinding({ investigationOutcome: outcome }))).toEqual(
|
||||
{ label: 'Fix failed', tone: 'danger' },
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it('returns investigating for a running investigation', () => {
|
||||
expect(
|
||||
getPatrolFindingActionableState(makeFinding({ investigationStatus: 'running' })),
|
||||
).toEqual({ label: 'Investigating', tone: 'info' });
|
||||
});
|
||||
|
||||
it('returns verifying fix for fix_executed', () => {
|
||||
expect(
|
||||
getPatrolFindingActionableState(makeFinding({ investigationOutcome: 'fix_executed' })),
|
||||
).toEqual({ label: 'Verifying fix', tone: 'info' });
|
||||
});
|
||||
|
||||
it('returns undefined for non-active findings', () => {
|
||||
expect(
|
||||
getPatrolFindingActionableState(
|
||||
makeFinding({ status: 'resolved', investigationOutcome: 'fix_queued' }),
|
||||
),
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -658,6 +658,35 @@ export function getPatrolWorkTypeCompositionClause(
|
|||
return ` — ${parts.join(', ')}`;
|
||||
}
|
||||
|
||||
export interface PatrolActionableStatePresentation {
|
||||
label: string;
|
||||
tone: MetadataBadgeTone;
|
||||
}
|
||||
|
||||
export function getPatrolFindingActionableState(
|
||||
finding: Pick<UnifiedFinding, 'status' | 'investigationStatus' | 'investigationOutcome'>,
|
||||
): PatrolActionableStatePresentation | undefined {
|
||||
if (finding.status !== 'active') return undefined;
|
||||
|
||||
if (finding.investigationOutcome === 'fix_queued') {
|
||||
return { label: 'Approval required', tone: 'warning' };
|
||||
}
|
||||
|
||||
if (isFailedFixOutcome(finding.investigationOutcome)) {
|
||||
return { label: 'Fix failed', tone: 'danger' };
|
||||
}
|
||||
|
||||
if (finding.investigationStatus === 'running') {
|
||||
return { label: 'Investigating', tone: 'info' };
|
||||
}
|
||||
|
||||
if (finding.investigationOutcome === 'fix_executed') {
|
||||
return { label: 'Verifying fix', tone: 'info' };
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export const getPatrolFindingIssueCountLabel = (count: number): string => {
|
||||
const normalized = Number.isFinite(count) ? Math.max(0, Math.trunc(count)) : 0;
|
||||
if (normalized === 1) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue