Files
fusion/packages/dashboard/app/utils/taskRecovery.ts
gsxdsm 50179ed5eb FN-8167: suppress failed UI during automatic recovery
Prevent transient automatic-retry tasks from appearing as terminal failures.

- Centralize pending-recovery and manual-retry presentation rules.
- Suppress failed styling, failure alerts, and Retry actions across list, card, and detail views.
- Cover recovery timing and desktop/mobile task surfaces with regression tests.

Files changed:
 .changeset/fn-8167-transient-retry-affordance.md   |  7 ++
 packages/dashboard/app/components/ListView.tsx     | 16 ++---
 packages/dashboard/app/components/TaskCard.tsx     | 13 ++--
 .../dashboard/app/components/TaskDetailModal.tsx   | 13 ++--
 .../app/components/__tests__/ListView.test.tsx     | 38 +++++++++++
 .../app/components/__tests__/TaskCard.test.tsx     | 22 +++++++
 .../__tests__/TaskDetailModal.rendering.test.tsx   | 26 ++++++++
 .../app/utils/__tests__/taskRecovery.test.ts       | 77 ++++++++++++++++++++++
 packages/dashboard/app/utils/taskRecovery.ts       | 32 +++++++++
 9 files changed, 215 insertions(+), 29 deletions(-)

Fusion-Task-Id: FN-8167

Fusion-Task-Lineage: 60d599e2-2e0e-46e7-ad16-cc6a836b5ac7

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-16 20:28:05 -07:00

33 lines
1.2 KiB
TypeScript

import type { Task } from "@fusion/core";
/**
* FNXC:TaskRecoveryAffordance 2026-07-16-12:00:
* FN-8167 treats a finite, strictly-future automatic recovery schedule as non-terminal.
* It wins over a stale `failed` status, so failed chrome and manual Retry render only
* after automatic recovery is no longer pending.
*/
export function hasPendingAutomaticRecovery(task: Task, nowMs = Date.now()): boolean {
const recoveryAtMs = Date.parse(task.nextRecoveryAt ?? "");
return Number.isFinite(recoveryAtMs) && recoveryAtMs > nowMs;
}
/**
* Determine whether a task needs a human-initiated retry.
*
* FNXC:TaskRecoveryAffordance 2026-07-16-12:00:
* A nonzero `recoveryRetryCount` and elapsed `nextRecoveryAt` do not themselves make a
* task retryable: elapsed or absent schedules fall back to terminal-status rules. A
* strictly-future schedule suppresses manual retry regardless of status.
*/
export function isTaskManuallyRetryable(task: Task, nowMs = Date.now()): boolean {
if (hasPendingAutomaticRecovery(task, nowMs)) {
return false;
}
return task.status === "failed"
|| task.status === "stuck-killed"
|| task.status === "planning"
|| task.status === "needs-replan"
|| (task.stuckKillCount ?? 0) > 0;
}