Keep board, list, and task detail planning indicators consistent with fresh lifecycle state. - preserve authoritative task snapshots across SSE and detail refreshes - render transient replan planning only for fresh, unpaused planner activity - pass global pause state to every task-detail host and cover status convergence Files changed: packages/dashboard/app/App.tsx | 2 + packages/dashboard/app/components/AppModals.tsx | 3 + packages/dashboard/app/components/ListView.tsx | 11 +- packages/dashboard/app/components/TaskCard.tsx | 7 +- .../dashboard/app/components/TaskDetailModal.tsx | 27 ++- .../app/components/__tests__/AppModals.test.tsx | 201 +++++++++++++++++++-- .../app/components/__tests__/ListView.test.tsx | 31 ++++ .../app/components/__tests__/TaskCard.test.tsx | 17 ++ .../app/components/dashboard/MainContent.tsx | 1 + .../__tests__/useTasks-hydration-freshness.test.ts | 58 +++++- .../dashboard/app/hooks/__tests__/useTasks.test.ts | 29 ++- packages/dashboard/app/hooks/useTasks.ts | 51 +++++- .../utils/__tests__/taskStatusBadgeLabel.test.ts | 22 ++- .../dashboard/app/utils/taskStatusBadgeLabel.ts | 36 ++++ 14 files changed, 453 insertions(+), 43 deletions(-) Fusion-Task-Id: FN-8798 Fusion-Task-Lineage: 7defae4e-fb39-4d2c-943a-ef6c1a164bfc Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
96 lines
4.2 KiB
TypeScript
96 lines
4.2 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import type { TFunction } from "i18next";
|
|
import { getTaskStatusBadgeLabel, hasTaskStatusBadge, isTaskPlanningActive, PLANNER_ACTIVITY_LIVE_WINDOW_MS } from "../taskStatusBadgeLabel";
|
|
|
|
const t = ((key: string, fallback?: string) => fallback ?? key) as TFunction<"app">;
|
|
|
|
describe("hasTaskStatusBadge", () => {
|
|
it.each([
|
|
"planning",
|
|
"executing",
|
|
"reviewing",
|
|
"merging",
|
|
"failed",
|
|
"needs-replan",
|
|
"done",
|
|
])("keeps a real status visible regardless of column placement: %s", (status) => {
|
|
expect(hasTaskStatusBadge(status)).toBe(true);
|
|
});
|
|
|
|
it("leaves null, undefined, and empty status badge-free", () => {
|
|
expect(hasTaskStatusBadge(null)).toBe(false);
|
|
expect(hasTaskStatusBadge(undefined)).toBe(false);
|
|
expect(hasTaskStatusBadge(" ")).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("isTaskPlanningActive", () => {
|
|
it("accepts planning status or fresh, unpaused planner activity on a replan card only", () => {
|
|
const now = Date.parse("2026-08-05T10:05:00.000Z");
|
|
expect(isTaskPlanningActive({ status: "planning" }, { globalPaused: true, now })).toBe(true);
|
|
expect(isTaskPlanningActive({ status: "needs-replan", recentAgentActivityAt: "2026-08-05T10:01:00.000Z" }, { now })).toBe(true);
|
|
expect(isTaskPlanningActive({ status: "needs-replan", recentAgentActivityAt: "2026-08-05T10:01:00.000Z" }, { globalPaused: true, now })).toBe(false);
|
|
expect(isTaskPlanningActive({ status: "needs-replan" }, { now })).toBe(false);
|
|
expect(isTaskPlanningActive({ status: undefined, recentAgentActivityAt: "2026-08-05T10:01:00.000Z" }, { now })).toBe(false);
|
|
});
|
|
|
|
it("expires historical or malformed planner activity", () => {
|
|
const now = Date.parse("2026-08-05T10:05:00.000Z");
|
|
expect(isTaskPlanningActive(
|
|
{ status: "needs-replan", recentAgentActivityAt: new Date(now - PLANNER_ACTIVITY_LIVE_WINDOW_MS - 1).toISOString() },
|
|
{ now },
|
|
)).toBe(false);
|
|
expect(isTaskPlanningActive({ status: "needs-replan", recentAgentActivityAt: "not-a-date" }, { now })).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("getTaskStatusBadgeLabel", () => {
|
|
it("maps the full AI merge pipeline to Merging…", () => {
|
|
for (const status of ["merging", "merging-pr", "reviewing", "landing"]) {
|
|
expect(getTaskStatusBadgeLabel(status, t)).toBe("Merging…");
|
|
}
|
|
});
|
|
|
|
it("keeps merging-fix distinct", () => {
|
|
expect(getTaskStatusBadgeLabel("merging-fix", t)).toBe("Merging fixes…");
|
|
});
|
|
|
|
it("keeps merging-fix over a still-running workflow-step label", () => {
|
|
// A pre-merge step's running state can survive into a merge-fix retry; the badge must not regress to the step name.
|
|
expect(getTaskStatusBadgeLabel("merging-fix", t, "Plan Review")).toBe("Merging fixes…");
|
|
});
|
|
|
|
it("keeps every active-merge status over a still-running workflow-step label", () => {
|
|
// The same stale startedAt-without-completedAt step state can survive into the whole merge pipeline.
|
|
for (const status of ["merging", "merging-pr", "reviewing", "landing"]) {
|
|
expect(getTaskStatusBadgeLabel(status, t, "Code Review")).toBe("Merging…");
|
|
}
|
|
});
|
|
|
|
it("lets a running workflow-step label override other statuses", () => {
|
|
expect(getTaskStatusBadgeLabel("planning", t, "Plan Review")).toBe("Plan Review");
|
|
expect(getTaskStatusBadgeLabel("needs-replan", t, "Plan Review")).toBe("Plan Review");
|
|
});
|
|
|
|
it("maps needs-replan to the operator-facing Revising label", () => {
|
|
const label = getTaskStatusBadgeLabel("needs-replan", t);
|
|
expect(label).toBe("Revising");
|
|
expect(label).not.toBe("Replan");
|
|
});
|
|
|
|
/*
|
|
FNXC:TaskStatusBadge 2026-07-26-14:05:
|
|
With the Plan Review gate badge naming itself, callers drop the workflow-step override while it
|
|
renders — so this branch is now what a planning card actually reads, and it must be operator copy
|
|
rather than the raw engine token it used to expose.
|
|
*/
|
|
it("maps the planning status to operator copy, not the engine token", () => {
|
|
expect(getTaskStatusBadgeLabel("planning", t)).toBe("Planning");
|
|
});
|
|
|
|
it("passes through non-merge statuses", () => {
|
|
expect(getTaskStatusBadgeLabel("failed", t)).toBe("failed");
|
|
expect(getTaskStatusBadgeLabel(null, t)).toBe("");
|
|
});
|
|
});
|