Files
fusion/packages/dashboard/app/utils/__tests__/taskActivity.test.ts
gsxdsm b6135f4bd5 FN-8494: keep task cards active while replanning
Keep task-card activity chrome visible throughout durable and fresh replanning states.

- Treat needs-replan tasks as agent-active on triage and todo lanes without changing lock policy.
- Cover board, list, mobile, pause, and freshness behaviors with regression tests.
- Add a patch changeset for the replanning activity indicator.

Files changed:
 .changeset/fn-8494-replan-active-glow.md           |  7 +++++
 .../app/components/__tests__/ListView.test.tsx     |  4 +++
 .../app/components/__tests__/TaskCard.test.tsx     | 31 ++++++++++++++++++++++
 .../app/utils/__tests__/taskActivity.test.ts       | 23 ++++++++++++++++
 packages/dashboard/app/utils/taskActivity.ts       |  7 ++++-
 5 files changed, 71 insertions(+), 1 deletion(-)

Fusion-Task-Id: FN-8494
Fusion-Task-Lineage: a910a22a-cff7-423f-82ee-359830beb104
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-22 13:57:02 -07:00

115 lines
5.7 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from "vitest";
import type { Task } from "@fusion/core";
import { ACTIVE_STATUSES, isTaskAgentActive } from "../taskActivity";
function makeTask(overrides: Partial<Task> = {}): Task {
return {
id: "FN-8055",
title: "Activity fixture",
description: "Activity fixture",
column: "triage",
status: null,
steps: [],
enabledWorkflowSteps: [],
workflowStepResults: [],
createdAt: "2026-07-16T00:00:00.000Z",
updatedAt: "2026-07-16T00:00:00.000Z",
...overrides,
} as Task;
}
function taskWithRunningWorkflowStep(overrides: Partial<Task> = {}): Task {
return makeTask({
enabledWorkflowSteps: ["plan-review"],
workflowStepResults: [{
workflowStepId: "plan-review",
workflowStepName: "Plan Review",
status: "pending",
startedAt: "2026-07-16T00:00:00.000Z",
}],
...overrides,
});
}
describe("isTaskAgentActive", () => {
afterEach(() => vi.useRealTimers());
it("uses the canonical set for every active phase", () => {
expect([...ACTIVE_STATUSES]).toEqual([
"planning", "researching", "executing", "finalizing", "merging", "merging-pr", "merging-fix", "reviewing", "landing",
]);
for (const status of ACTIVE_STATUSES) {
expect(isTaskAgentActive(makeTask({ status }))).toBe(true);
}
});
it("recognizes an in-progress task and status-null running workflow step", () => {
expect(isTaskAgentActive(makeTask({ column: "in-progress" }))).toBe(true);
expect(isTaskAgentActive(taskWithRunningWorkflowStep())).toBe(true);
});
it("keeps durable replan cards active without changing lock statuses", () => {
expect(ACTIVE_STATUSES.has("needs-replan")).toBe(false);
expect(isTaskAgentActive(makeTask({ status: "needs-replan", column: "triage" }))).toBe(true);
expect(isTaskAgentActive(makeTask({ status: "needs-replan", column: "todo" }))).toBe(true);
});
it("does not treat a status-null task without a running item as active", () => {
expect(isTaskAgentActive(makeTask())).toBe(false);
});
it("uses a fresh client-only planner log signal for a status-null triage card", () => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-07-28T12:00:30.000Z"));
expect(isTaskAgentActive(makeTask({
recentAgentActivityAt: "2026-07-28T12:00:00.000Z",
}))).toBe(true);
expect(isTaskAgentActive(makeTask({
recentAgentActivityAt: "2026-07-28T11:59:00.000Z",
}))).toBe(false);
});
it("extends fresh planner activity to plan-in-place todo replans", () => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-07-22T09:25:30.000Z"));
expect(isTaskAgentActive(makeTask({
column: "todo",
status: "needs-replan",
recentAgentActivityAt: "2026-07-22T09:25:00.000Z",
}))).toBe(true);
});
it.each([
["queued task status", taskWithRunningWorkflowStep({ status: "queued" }), {}],
["paused status", taskWithRunningWorkflowStep({ status: "paused" }), {}],
["paused task", taskWithRunningWorkflowStep({ paused: true }), {}],
["failed status", taskWithRunningWorkflowStep({ status: "failed" }), {}],
["stuck-killed status", taskWithRunningWorkflowStep({ column: "in-progress", status: "stuck-killed" }), {}],
["awaiting approval", taskWithRunningWorkflowStep({ status: "awaiting-approval" }), {}],
["awaiting user input", taskWithRunningWorkflowStep({ status: "awaiting-user-input" }), {}],
["done status", taskWithRunningWorkflowStep({ status: "done" }), {}],
["done column", taskWithRunningWorkflowStep({ column: "done" }), {}],
["archived column with merging status", taskWithRunningWorkflowStep({ column: "archived", status: "merging" }), {}],
["archived column with running workflow", taskWithRunningWorkflowStep({ column: "archived" }), {}],
["render queue", taskWithRunningWorkflowStep(), { queued: true }],
["derived stuck", taskWithRunningWorkflowStep(), { isStuck: true }],
["global pause", taskWithRunningWorkflowStep(), { globalPaused: true }],
["failed status with fresh planner log", makeTask({ status: "failed", recentAgentActivityAt: new Date().toISOString() }), {}],
["paused task with fresh planner log", makeTask({ paused: true, recentAgentActivityAt: new Date().toISOString() }), {}],
["done column with fresh planner log", makeTask({ column: "done", recentAgentActivityAt: new Date().toISOString() }), {}],
["archived column with fresh planner log", makeTask({ column: "archived", recentAgentActivityAt: new Date().toISOString() }), {}],
["awaiting approval with fresh planner log", makeTask({ status: "awaiting-approval", recentAgentActivityAt: new Date().toISOString() }), {}],
["awaiting user input with fresh planner log", makeTask({ status: "awaiting-user-input", recentAgentActivityAt: new Date().toISOString() }), {}],
["queued replan", makeTask({ status: "needs-replan", recentAgentActivityAt: new Date().toISOString() }), { queued: true }],
["derived stuck replan", makeTask({ status: "needs-replan", recentAgentActivityAt: new Date().toISOString() }), { isStuck: true }],
["global pause replan", makeTask({ status: "needs-replan", recentAgentActivityAt: new Date().toISOString() }), { globalPaused: true }],
["paused replan", makeTask({ status: "needs-replan", paused: true, recentAgentActivityAt: new Date().toISOString() }), {}],
["failed replan", makeTask({ status: "failed", recentAgentActivityAt: new Date().toISOString() }), {}],
["done-column replan", makeTask({ column: "done", status: "needs-replan", recentAgentActivityAt: new Date().toISOString() }), {}],
] as const)("rejects %s before running workflow activity", (_name, task, options) => {
expect(isTaskAgentActive(task, options)).toBe(false);
});
});