Files
fusion/packages/dashboard/app/utils/__tests__/taskRecovery.test.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

78 lines
3.1 KiB
TypeScript

import { describe, expect, it } from "vitest";
import type { Task } from "@fusion/core";
import { hasPendingAutomaticRecovery, isTaskManuallyRetryable } from "../taskRecovery";
const nowMs = Date.parse("2026-07-16T12:00:00.000Z");
function makeTask(overrides: Partial<Task> = {}): Task {
return {
id: "FN-8167",
title: "Recovery fixture",
description: "",
column: "todo",
steps: [],
dependencies: [],
status: undefined,
...overrides,
} as Task;
}
describe("task recovery presentation", () => {
it("recognizes only finite strictly-future recovery schedules as pending", () => {
expect(hasPendingAutomaticRecovery(makeTask({ nextRecoveryAt: new Date(nowMs + 1).toISOString() }), nowMs)).toBe(true);
expect(hasPendingAutomaticRecovery(makeTask({ nextRecoveryAt: new Date(nowMs).toISOString() }), nowMs)).toBe(false);
expect(hasPendingAutomaticRecovery(makeTask({ nextRecoveryAt: new Date(nowMs - 1).toISOString() }), nowMs)).toBe(false);
expect(hasPendingAutomaticRecovery(makeTask({ nextRecoveryAt: "not-a-date" }), nowMs)).toBe(false);
expect(hasPendingAutomaticRecovery(makeTask({ nextRecoveryAt: null }), nowMs)).toBe(false);
});
it("suppresses manual retry for future automatic recovery in active columns", () => {
for (const column of ["todo", "in-progress"] as const) {
const task = makeTask({
column,
recoveryRetryCount: 1,
nextRecoveryAt: new Date(nowMs + 60_000).toISOString(),
});
expect(hasPendingAutomaticRecovery(task, nowMs), column).toBe(true);
expect(isTaskManuallyRetryable(task, nowMs), column).toBe(false);
}
});
it("lets a future schedule win over a defensive stale failed status", () => {
const task = makeTask({
status: "failed",
recoveryRetryCount: 1,
nextRecoveryAt: new Date(nowMs + 60_000).toISOString(),
});
expect(hasPendingAutomaticRecovery(task, nowMs)).toBe(true);
expect(isTaskManuallyRetryable(task, nowMs)).toBe(false);
});
it("falls back to terminal retry states after recovery is elapsed or unscheduled", () => {
expect(isTaskManuallyRetryable(makeTask({
status: "failed",
recoveryRetryCount: 1,
nextRecoveryAt: new Date(nowMs - 60_000).toISOString(),
}), nowMs)).toBe(true);
expect(isTaskManuallyRetryable(makeTask({ status: "failed", recoveryRetryCount: 1 }), nowMs)).toBe(true);
expect(isTaskManuallyRetryable(makeTask({ status: "failed", recoveryRetryCount: null, nextRecoveryAt: null }), nowMs)).toBe(true);
});
it("keeps established terminal retry states retryable without a pending schedule", () => {
for (const task of [
makeTask({ status: "stuck-killed" }),
makeTask({ status: "needs-replan" }),
makeTask({ status: "planning" }),
makeTask({ stuckKillCount: 1 }),
]) {
expect(isTaskManuallyRetryable(task, nowMs)).toBe(true);
}
});
it("never infers retryability for done or archived tasks without terminal state", () => {
expect(isTaskManuallyRetryable(makeTask({ column: "done" }), nowMs)).toBe(false);
expect(isTaskManuallyRetryable(makeTask({ column: "archived" }), nowMs)).toBe(false);
});
});