mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-25 21:20:00 +00:00
Hide Re-investigate while a planned fix is queued
A Patrol investigation can plan a fix and then fail, normally on the final summary call. The run then shows a queued fix with a failed status, and the Investigation section offered Re-investigate because it only looked at the status. Another run would plan a second action for the same problem while the first still waits for approval. The section now hides Re-investigate while the outcome is fix_queued, as it already did for a completed run with a queued fix. The failed run's note uses the muted style of an interrupted run, because the fix awaiting approval is what the operator acts on, not the error. The Pro orchestrator keeps the plan's outcome on such a run and refuses the re-investigation request too.
This commit is contained in:
@@ -2817,3 +2817,18 @@ now starts a targeted Patrol run, so alert follow-up lands on the Patrol page.
|
||||
`patrolInvestigationContextModel.ts` shrank to the investigation-record
|
||||
presentation that `InvestigationSection` uses, and the Patrol tool transcript
|
||||
block moved to `frontend-modern/src/components/patrol/ToolExecutionBlock.tsx`.
|
||||
|
||||
### Failed run with a planned fix (23 October 2026)
|
||||
|
||||
An investigation can persist a canonical action plan and then fail, normally
|
||||
on the final summary call. The orchestrator (in pulse-pro) keeps the plan's
|
||||
outcome, normally `fix_queued`, on the failed run instead of `timed_out` or
|
||||
`interrupted`, so the retry gates leave the finding alone rather than plan the
|
||||
same fix again, and it refuses a manual re-investigation while the latest
|
||||
run's outcome is `fix_queued` (409 `action_pending`). The Investigation section
|
||||
matches: it offers no Re-investigate while the outcome is `fix_queued`,
|
||||
whatever the run status, and the failed run's note renders as information, as
|
||||
for an interrupted run, because the queued fix is what the operator acts on.
|
||||
Once core reconciles the plan's approval, rejection or expiry, the outcome
|
||||
changes and the usual rules apply again. Proof:
|
||||
`frontend-modern/src/components/patrol/__tests__/InvestigationSection.test.tsx`.
|
||||
|
||||
@@ -44,10 +44,11 @@ const INVESTIGATION_BADGE_PROPS = {
|
||||
const summaryClass =
|
||||
'text-sm prose prose-slate prose-sm dark:prose-invert max-w-none wrap-break-word prose-headings:my-2 prose-p:my-2 prose-pre:overflow-x-auto prose-code:break-all prose-code:before:content-none prose-code:after:content-none';
|
||||
|
||||
// A run cut short by a restart did not fail: Patrol runs it again, so its note
|
||||
// reads as information rather than as an error.
|
||||
// A run cut short by a restart did not fail: Patrol runs it again. A run that
|
||||
// stopped after planning a fix left that fix waiting for approval. Either way,
|
||||
// its note reads as information rather than as an error.
|
||||
const runNoteClass = (outcome: string | undefined): string =>
|
||||
outcome === 'interrupted'
|
||||
outcome === 'interrupted' || outcome === 'fix_queued'
|
||||
? 'border-border-subtle bg-surface text-muted'
|
||||
: 'border-red-200 bg-red-50 text-red-700 dark:border-red-800 dark:bg-red-900/25 dark:text-red-300';
|
||||
|
||||
@@ -102,6 +103,9 @@ export const InvestigationSection: Component<InvestigationSectionProps> = (props
|
||||
const inv = investigation();
|
||||
if (!inv) return true; // No investigation yet
|
||||
if (inv.status === 'running') return false;
|
||||
// A queued fix is still open, even after a run that failed later.
|
||||
// Investigating again would plan a second action for the same problem.
|
||||
if (inv.outcome === 'fix_queued') return false;
|
||||
return (
|
||||
inv.status === 'failed' ||
|
||||
inv.status === 'needs_attention' ||
|
||||
|
||||
@@ -163,6 +163,29 @@ describe('InvestigationSection', () => {
|
||||
expect(screen.getByRole('button', { name: 'Re-investigate' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not offer another run while a planned fix is queued', async () => {
|
||||
getInvestigationMock.mockResolvedValue({
|
||||
id: 'inv-queued',
|
||||
finding_id: 'finding-queued',
|
||||
session_id: 'session-queued',
|
||||
status: 'failed',
|
||||
outcome: 'fix_queued',
|
||||
started_at: '2026-10-23T10:00:00.000Z',
|
||||
turn_count: 6,
|
||||
error: 'The provider stopped responding.',
|
||||
} satisfies Investigation);
|
||||
|
||||
render(() => <InvestigationSection findingId="finding-queued" investigationStatus="failed" />);
|
||||
|
||||
// The run failed after planning a fix; the fix is still waiting for approval.
|
||||
await screen.findByText(getInvestigationOutcomeLabel('fix_queued'));
|
||||
expect(screen.queryByRole('button', { name: 'Re-investigate' })).not.toBeInTheDocument();
|
||||
// The run's note is information beside the queued fix, not an alarm.
|
||||
const noteBox = screen.getByText('The provider stopped responding.');
|
||||
expect(noteBox.className).toContain('text-muted');
|
||||
expect(noteBox.className).not.toContain('bg-red-50');
|
||||
});
|
||||
|
||||
it('keeps a failed run error in the error style', async () => {
|
||||
getInvestigationMock.mockResolvedValue({
|
||||
id: 'inv-failed',
|
||||
|
||||
Reference in New Issue
Block a user