Catch up frontend tests with collapsed Patrol details and refactored FindingsPanel

This commit is contained in:
rcourtman
2026-05-11 23:37:21 +01:00
parent 2725a325f2
commit edda19aadf
4 changed files with 64 additions and 31 deletions
@@ -65,6 +65,7 @@ import {
getFindingSubjectPresentation,
getFindingTitlePresentation,
getFindingRecencyPresentation,
findingHasAppliedFix,
hasFindingInvestigationDetails,
hasFindingInvestigationHandoffPointer,
getInvestigationConfidenceBadgeClasses,
@@ -632,23 +633,9 @@ export const FindingsPanel: Component<FindingsPanelProps> = (props) => {
await openFindingInAssistant(finding, 'verify_fix');
};
// True when the finding has an investigation outcome indicating that
// some remediation step has run against it — anything past "fix
// queued." For these states, Verify fix is a meaningful action; for
// fix_queued (still awaiting approval) and earlier states there is
// nothing applied yet to verify, and for fix_failed the fix didn't
// complete so verification doesn't apply.
const hasAppliedFix = (finding: UnifiedFinding): boolean => {
switch (finding.investigationOutcome) {
case 'fix_executed':
case 'fix_verified':
case 'fix_verification_failed':
case 'fix_verification_unknown':
return true;
default:
return false;
}
};
// hasAppliedFix logic lives in @/utils/aiFindingPresentation as
// findingHasAppliedFix so the investigation-outcome switch stays in a
// shared adapter (per the resource-type-boundaries test).
// Create rule from this is the promotion path: take the operator's
// implicit pattern (silencing the same {resource, category} pair
@@ -1333,7 +1320,7 @@ export const FindingsPanel: Component<FindingsPanelProps> = (props) => {
</svg>
Why
</button>
<Show when={hasAppliedFix(finding) && finding.status === 'active'}>
<Show when={findingHasAppliedFix(finding) && finding.status === 'active'}>
<button
type="button"
onClick={(e) => handleVerifyFixFinding(finding, e)}
@@ -1428,15 +1415,17 @@ export const FindingsPanel: Component<FindingsPanelProps> = (props) => {
Acknowledge
</button>
</Show>
<button
type="button"
onClick={(e) => handleResolve(finding, e)}
class="px-2 py-1 rounded border border-emerald-200 text-emerald-700 hover:bg-emerald-50 dark:border-emerald-800 dark:text-emerald-300 dark:hover:bg-emerald-900"
title="Mark this finding as resolved. Use when you have fixed the issue out-of-band."
disabled={actionLoading() === finding.id}
>
Mark resolved
</button>
<Show when={manualControls.acknowledge && finding.status === 'active'}>
<button
type="button"
onClick={(e) => handleResolve(finding, e)}
class="px-2 py-1 rounded border border-emerald-200 text-emerald-700 hover:bg-emerald-50 dark:border-emerald-800 dark:text-emerald-300 dark:hover:bg-emerald-900"
title="Mark this finding as resolved. Use when you have fixed the issue out-of-band."
disabled={actionLoading() === finding.id}
>
Mark resolved
</button>
</Show>
</div>
</Show>
<Show when={manualControls.snooze}>
@@ -51,9 +51,12 @@ describe('FindingsPanel assistant handoff', () => {
expect(findingsPanelSource).toContain('buildPatrolAssistantFindingHandoff');
expect(findingsPanelSource).toContain('buildPatrolAssistantApprovalBriefingInput');
expect(findingsPanelSource).toContain('buildPatrolAssistantProposedFixBriefingInput');
expect(findingsPanelSource).toContain(
'aiChatStore.openWithPrompt(handoff.prompt, handoff.context)',
);
// openWithPrompt receives the handoff context spread inline (so the call
// site can layer autoSendInitialPrompt for verb-style entrypoints like
// Investigate / Why / Verify fix). The base handoff.context must still
// flow through.
expect(findingsPanelSource).toContain('aiChatStore.openWithPrompt(handoff.prompt, {');
expect(findingsPanelSource).toContain('...handoff.context');
expect(findingsPanelSource).toContain('investigationOutcome: finding.investigationOutcome');
expect(findingsPanelSource).toContain('investigationStatus: finding.investigationStatus');
expect(findingsPanelSource).toContain('remediationId: finding.remediationPlanId');
@@ -1154,9 +1154,17 @@ describe('AIIntelligence entitlement gating', () => {
expect(screen.getByTestId('patrol-recommended-next-step-action')).toHaveTextContent(
'Run Patrol',
);
expect(findingsPanelState.latestProps).not.toBeNull();
});
// Patrol summary verification + activity-mix copy live behind the
// "Show details" toggle since commit 174d2b04d. Expand the section so
// the deeper assertions can find the text.
fireEvent.click(screen.getByTestId('patrol-summary-details-toggle'));
await waitFor(() => {
expect(screen.getByText('No recent full patrol')).toBeInTheDocument();
expect(screen.getAllByText(/Last activity/i)).toHaveLength(1);
expect(findingsPanelState.latestProps).not.toBeNull();
});
expect(screen.queryByText('No issues found')).not.toBeInTheDocument();
@@ -1328,6 +1336,13 @@ describe('AIIntelligence entitlement gating', () => {
'Patrol has an active runtime issue: Provider billing or quota issue. Recent coverage is also incomplete, so the rest of your infrastructure is not fully verified.',
),
).toBeInTheDocument();
});
// "Runtime issues" and "Latest activity" both live in the collapsible
// Patrol summary detail section (commit 174d2b04d).
fireEvent.click(screen.getByTestId('patrol-summary-details-toggle'));
await waitFor(() => {
expect(screen.getByText('Runtime issues')).toBeInTheDocument();
expect(screen.getByText('Latest activity')).toBeInTheDocument();
});
@@ -1486,6 +1501,13 @@ describe('AIIntelligence entitlement gating', () => {
render(() => <AIIntelligence />);
// "Recent activity mix" copy lives in the collapsible Patrol summary
// detail section (commit 174d2b04d). Expand before asserting.
await waitFor(() => {
expect(screen.getByTestId('patrol-summary-details-toggle')).toBeInTheDocument();
});
fireEvent.click(screen.getByTestId('patrol-summary-details-toggle'));
await waitFor(() => {
expect(
screen.getByText('Recent activity mix: 1 full, 1 alert-triggered, 1 anomaly-triggered'),
@@ -617,6 +617,25 @@ export const doesFindingNeedAttention = (
);
};
// True when the finding has an investigation outcome indicating that some
// remediation step has run against it — anything past "fix queued." For these
// states, Verify fix is a meaningful action; for fix_queued (still awaiting
// approval) and earlier states there is nothing applied yet to verify, and
// for fix_failed the fix didn't complete so verification doesn't apply.
export const findingHasAppliedFix = (
finding: Pick<UnifiedFinding, 'investigationOutcome'>,
): boolean => {
switch (finding.investigationOutcome) {
case 'fix_executed':
case 'fix_verified':
case 'fix_verification_failed':
case 'fix_verification_unknown':
return true;
default:
return false;
}
};
export const getFindingLoopStateBadgeClasses = (loopState: string): string =>
FINDING_LOOP_STATE_CLASSES[loopState] || DEFAULT_LOOP_STATE_CLASSES;