FN-7831: add Reviewing badge for active Plan Review on task cards and list rows
Adds a distinct "Reviewing" status badge that surfaces on TaskCard and ListView rows while a task's optional plan-review workflow step is actively running, reusing the unified progress predicate so board and list surfaces stay in sync. - Add isPlanReviewRunning(task) helper in taskProgress.ts, derived from getUnifiedTaskProgress's workflow-plan-review item status - Render a pulsing "Reviewing" badge on TaskCard header (additive to existing status badges, with title/data-testid) while plan-review is running - Render the matching "Reviewing" badge on both grouped and ungrouped ListView row layouts for parity with TaskCard - Add supporting CSS for .card-status-badge--reviewing and .list-status-badge--reviewing - Add unit tests for isPlanReviewRunning and component tests for the new badge across TaskCard and ListView - Add minor changeset documenting the new operator-facing badge Files changed: .changeset/tidy-reviewing-badges.md | 7 ++ packages/dashboard/app/components/ListView.css | 10 +++ packages/dashboard/app/components/ListView.tsx | 22 +++++- packages/dashboard/app/components/TaskCard.css | 10 +++ packages/dashboard/app/components/TaskCard.tsx | 25 ++++++- .../app/components/__tests__/ListView.test.tsx | 78 ++++++++++++++++++++++ .../app/components/__tests__/TaskCard.test.tsx | 31 +++++++++ .../app/utils/__tests__/taskProgress.test.ts | 16 ++++- packages/dashboard/app/utils/taskProgress.ts | 10 +++ 9 files changed, 205 insertions(+), 4 deletions(-) Fusion-Task-Id: FN-7831 Fusion-Task-Lineage: d36f8c63-9b84-400a-8b10-3b9f3b04212b Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/tidy-reviewing-badges.md
Normal file
7
.changeset/tidy-reviewing-badges.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
summary: Task cards now show a "Reviewing" badge while a task is in Plan Review.
|
||||||
|
category: feature
|
||||||
|
dev: Adds isPlanReviewRunning(task) in taskProgress.ts; consumed by TaskCard + ListView status badges.
|
||||||
@@ -741,6 +741,16 @@ In the split sidebar the title cell must allow the title to wrap to two lines (h
|
|||||||
background: var(--status-in-review-bg);
|
background: var(--status-in-review-bg);
|
||||||
color: var(--in-review);
|
color: var(--in-review);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
FNXC:TaskCardPlanReviewBadge 2026-07-11-12:16:
|
||||||
|
ListView uses the same active-review token as TaskCard for the FN-7831 Reviewing badge. Keep the modifier styling token-only and geometry-neutral so grouped cards and table rows preserve existing responsive layout.
|
||||||
|
*/
|
||||||
|
.list-status-badge--reviewing {
|
||||||
|
background: color-mix(in srgb, var(--in-review) 18%, transparent);
|
||||||
|
color: var(--in-review);
|
||||||
|
box-shadow: inset 0 0 0 var(--btn-border-width) color-mix(in srgb, var(--in-review) 35%, transparent);
|
||||||
|
}
|
||||||
.list-status-badge--done {
|
.list-status-badge--done {
|
||||||
background: var(--status-done-bg);
|
background: var(--status-done-bg);
|
||||||
color: var(--done);
|
color: var(--done);
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import type { ToastType } from "../hooks/useToast";
|
|||||||
import { useViewportMode } from "../hooks/useViewportMode";
|
import { useViewportMode } from "../hooks/useViewportMode";
|
||||||
import { getScopedItem, removeScopedItem, setScopedItem } from "../utils/projectStorage";
|
import { getScopedItem, removeScopedItem, setScopedItem } from "../utils/projectStorage";
|
||||||
import { ALL_WORKFLOWS_BOARD_VIEW_ID } from "../utils/boardWorkflowSelection";
|
import { ALL_WORKFLOWS_BOARD_VIEW_ID } from "../utils/boardWorkflowSelection";
|
||||||
import { getUnifiedTaskProgress } from "../utils/taskProgress";
|
import { getUnifiedTaskProgress, isPlanReviewRunning } from "../utils/taskProgress";
|
||||||
import { useConfirm } from "../hooks/useConfirm";
|
import { useConfirm } from "../hooks/useConfirm";
|
||||||
import { extractDependencyDeleteConflict, extractLineageDeleteConflict } from "../utils/taskDelete";
|
import { extractDependencyDeleteConflict, extractLineageDeleteConflict } from "../utils/taskDelete";
|
||||||
import { WorkflowSwitcher } from "./WorkflowSwitcher";
|
import { WorkflowSwitcher } from "./WorkflowSwitcher";
|
||||||
@@ -2596,6 +2596,7 @@ export function ListView({
|
|||||||
!isStuckState &&
|
!isStuckState &&
|
||||||
(task.column === "in-progress" || ACTIVE_STATUSES.has(visualStatus as string));
|
(task.column === "in-progress" || ACTIVE_STATUSES.has(visualStatus as string));
|
||||||
const hasStatus = typeof visualStatus === "string" && visualStatus.trim().length > 0;
|
const hasStatus = typeof visualStatus === "string" && visualStatus.trim().length > 0;
|
||||||
|
const planReviewRunning = isPlanReviewRunning(task);
|
||||||
const hasDependencies = Boolean(task.dependencies && task.dependencies.length > 0);
|
const hasDependencies = Boolean(task.dependencies && task.dependencies.length > 0);
|
||||||
const taskProgress = getTaskProgress(task);
|
const taskProgress = getTaskProgress(task);
|
||||||
const hasProgress = taskProgress.hasProgress;
|
const hasProgress = taskProgress.hasProgress;
|
||||||
@@ -2654,6 +2655,15 @@ export function ListView({
|
|||||||
{getTaskStatusLabel(visualStatus ?? "", t)}
|
{getTaskStatusLabel(visualStatus ?? "", t)}
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
|
{planReviewRunning && (
|
||||||
|
/*
|
||||||
|
FNXC:TaskCardPlanReviewBadge 2026-07-11-12:10:
|
||||||
|
Grouped ListView cards must show the same active Plan Review "Reviewing" badge as TaskCard so board and list surfaces remain visually equivalent while the `plan-review` workflow step is running.
|
||||||
|
*/
|
||||||
|
<span className="list-status-badge list-status-badge--reviewing pulsing">
|
||||||
|
{t("listView.reviewing", "Reviewing")}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="list-card-row">
|
<div className="list-card-row">
|
||||||
@@ -2796,6 +2806,7 @@ export function ListView({
|
|||||||
!isPaused &&
|
!isPaused &&
|
||||||
!isStuckState &&
|
!isStuckState &&
|
||||||
(task.column === "in-progress" || ACTIVE_STATUSES.has(visualStatus as string));
|
(task.column === "in-progress" || ACTIVE_STATUSES.has(visualStatus as string));
|
||||||
|
const planReviewRunning = isPlanReviewRunning(task);
|
||||||
const isDragging = draggingTaskId === task.id;
|
const isDragging = draggingTaskId === task.id;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -2870,6 +2881,15 @@ export function ListView({
|
|||||||
) : (
|
) : (
|
||||||
<span className="list-status-badge">-</span>
|
<span className="list-status-badge">-</span>
|
||||||
)}
|
)}
|
||||||
|
{planReviewRunning && (
|
||||||
|
/*
|
||||||
|
FNXC:TaskCardPlanReviewBadge 2026-07-11-12:11:
|
||||||
|
Ungrouped ListView table rows must render the same Reviewing badge from the shared predicate; this second status render path is easy to miss and must stay in parity with grouped rows.
|
||||||
|
*/
|
||||||
|
<span className="list-status-badge list-status-badge--reviewing pulsing">
|
||||||
|
{t("listView.reviewing", "Reviewing")}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</td>
|
</td>
|
||||||
)}
|
)}
|
||||||
{visibleColumns.has("column") && (
|
{visibleColumns.has("column") && (
|
||||||
|
|||||||
@@ -292,6 +292,16 @@ FN-7780 anchors the created-by-agent badge in its own bottom-left row before wor
|
|||||||
color: var(--color-error);
|
color: var(--color-error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
FNXC:TaskCardPlanReviewBadge 2026-07-11-12:15:
|
||||||
|
The Plan Review "Reviewing" badge must read as an active review state without adding custom sizing. Reuse the in-review semantic token and existing status badge geometry so desktop and mobile header wrapping keep the same invariants.
|
||||||
|
*/
|
||||||
|
.card-status-badge--reviewing {
|
||||||
|
background: color-mix(in srgb, var(--in-review) 18%, transparent);
|
||||||
|
color: var(--in-review);
|
||||||
|
border-color: color-mix(in srgb, var(--in-review) 35%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
.card-status-badge.stalled-review {
|
.card-status-badge.stalled-review {
|
||||||
background: color-mix(in srgb, var(--color-warning) 18%, transparent);
|
background: color-mix(in srgb, var(--color-warning) 18%, transparent);
|
||||||
color: var(--color-warning);
|
color: var(--color-warning);
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ import { getStalledReviewSignal } from "../utils/taskStalledReview";
|
|||||||
import { getInReviewStallCopy, shouldShowInReviewStallBadge } from "../utils/inReviewStallCopy";
|
import { getInReviewStallCopy, shouldShowInReviewStallBadge } from "../utils/inReviewStallCopy";
|
||||||
import { getStalePausedReviewCopy, shouldShowStalePausedReviewBadge } from "../utils/stalePausedReviewCopy";
|
import { getStalePausedReviewCopy, shouldShowStalePausedReviewBadge } from "../utils/stalePausedReviewCopy";
|
||||||
import { getTaskAgeStalenessCopy, shouldShowTaskAgeStalenessBadge } from "../utils/taskAgeStalenessCopy";
|
import { getTaskAgeStalenessCopy, shouldShowTaskAgeStalenessBadge } from "../utils/taskAgeStalenessCopy";
|
||||||
import { getUnifiedTaskProgress } from "../utils/taskProgress";
|
import { getUnifiedTaskProgress, isPlanReviewRunning } from "../utils/taskProgress";
|
||||||
import { getPrBadgeModifierClass } from "../utils/prBadgeClass";
|
import { getPrBadgeModifierClass } from "../utils/prBadgeClass";
|
||||||
import { getActiveRuntimeMs, getEndToEndDurationMs, getTimedDurationMs, getWorkflowRuntimeMs, parseTimestampToMs } from "../utils/taskTiming";
|
import { getActiveRuntimeMs, getEndToEndDurationMs, getTimedDurationMs, getWorkflowRuntimeMs, parseTimestampToMs } from "../utils/taskTiming";
|
||||||
import { canStartPrFeedbackAddressing, getTaskPrimaryPrInfo } from "../utils/prFeedback";
|
import { canStartPrFeedbackAddressing, getTaskPrimaryPrInfo } from "../utils/prFeedback";
|
||||||
@@ -1274,6 +1274,14 @@ function TaskCardComponent({
|
|||||||
const stalledReview = getStalledReviewSignal(task);
|
const stalledReview = getStalledReviewSignal(task);
|
||||||
const showStalledReview = Boolean(stalledReview && task.column === "in-review" && !isPaused);
|
const showStalledReview = Boolean(stalledReview && task.column === "in-review" && !isPaused);
|
||||||
const hasInReviewStall = shouldShowInReviewStallBadge(task);
|
const hasInReviewStall = shouldShowInReviewStallBadge(task);
|
||||||
|
/*
|
||||||
|
FNXC:TaskCardPlanReviewBadge 2026-07-11-12:05:
|
||||||
|
FN-7831 requires the card header to show a distinct "Reviewing" badge while the optional `plan-review` workflow step is actively running, even while the card remains in Planning/`triage`. Use the shared predicate so TaskCard stays in sync with ListView.
|
||||||
|
*/
|
||||||
|
const planReviewRunning = useMemo(
|
||||||
|
() => isPlanReviewRunning(task),
|
||||||
|
[task.steps, task.enabledWorkflowSteps, task.workflowStepResults],
|
||||||
|
);
|
||||||
// CLI agent session badges (U11) — distinct from staleness/stall badges.
|
// CLI agent session badges (U11) — distinct from staleness/stall badges.
|
||||||
const cliWaitingOnInput = cliSessionState?.agentState === "waitingOnInput";
|
const cliWaitingOnInput = cliSessionState?.agentState === "waitingOnInput";
|
||||||
const cliNeedsAttention = cliSessionState?.agentState === "needsAttention";
|
const cliNeedsAttention = cliSessionState?.agentState === "needsAttention";
|
||||||
@@ -1389,7 +1397,7 @@ function TaskCardComponent({
|
|||||||
);
|
);
|
||||||
/*
|
/*
|
||||||
FNXC:TaskCardWorkflowProgress 2026-07-08-hh:mm:
|
FNXC:TaskCardWorkflowProgress 2026-07-08-hh:mm:
|
||||||
FN-7676 — cards in the Planning/`triage` column must not surface the steps breakdown (progress bar, active badge, step-count toggle, expandable list); enumerated implementation steps are premature planning artifacts, not execution progress. The affordance now appears only after the task leaves Planning (`in-progress` / `executing`), matching `ListView.shouldShowTaskProgress`. A running Plan Review while still in `triage` intentionally no longer surfaces the card progress indicator — the header `planning` status badge remains the only in-flight signal.
|
FN-7676 — cards in the Planning/`triage` column must not surface the steps breakdown (progress bar, active badge, step-count toggle, expandable list); enumerated implementation steps are premature planning artifacts, not execution progress. The affordance now appears only after the task leaves Planning (`in-progress` / `executing`), matching `ListView.shouldShowTaskProgress`. FN-7831 adds a separate header "Reviewing" badge for a running Plan Review, but the progress breakdown itself remains hidden in Planning.
|
||||||
*/
|
*/
|
||||||
const showProgressSection =
|
const showProgressSection =
|
||||||
unifiedProgress.total > 0 && (task.status === "executing" || task.column === "in-progress");
|
unifiedProgress.total > 0 && (task.status === "executing" || task.column === "in-progress");
|
||||||
@@ -2840,6 +2848,19 @@ function TaskCardComponent({
|
|||||||
{isStuck ? t("tasks.stuck", "Stuck") : isAwaitingApproval ? t("tasks.awaitingApproval", "Awaiting Approval") : isAwaitingInput ? t("tasks.needsInput", "Needs input") : visualStatus === "merging-fix" ? t("tasks.statusMergingFix", "Merging fixes…") : getTaskStatusLabel(visualStatus, t)}
|
{isStuck ? t("tasks.stuck", "Stuck") : isAwaitingApproval ? t("tasks.awaitingApproval", "Awaiting Approval") : isAwaitingInput ? t("tasks.needsInput", "Needs input") : visualStatus === "merging-fix" ? t("tasks.statusMergingFix", "Merging fixes…") : getTaskStatusLabel(visualStatus, t)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
{planReviewRunning && (
|
||||||
|
/*
|
||||||
|
FNXC:TaskCardPlanReviewBadge 2026-07-11-12:06:
|
||||||
|
The Reviewing badge is additive to the normal header status badge so operators can distinguish "planning" from active Plan Review without hiding paused/stuck/status affordances.
|
||||||
|
*/
|
||||||
|
<span
|
||||||
|
className="card-status-badge card-status-badge--reviewing pulsing"
|
||||||
|
data-testid={`card-reviewing-${task.id}`}
|
||||||
|
title={t("tasks.planReviewingTitle", "Plan Review in progress")}
|
||||||
|
>
|
||||||
|
{t("tasks.reviewing", "Reviewing")}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
{/*
|
{/*
|
||||||
FNXC:CodingIdeasWorkflow 2026-07-04-11:10:
|
FNXC:CodingIdeasWorkflow 2026-07-04-11:10:
|
||||||
In the merged planner/capacity "todo" column (Coding (Ideas)), a planned task with no active status is ready and waiting for an in-progress slot. Show a "Ready" badge so operators can distinguish planned cards from freshly promoted unplanned ones. Tasks still being planned surface the "planning" status badge above instead.
|
In the merged planner/capacity "todo" column (Coding (Ideas)), a planned task with no active status is ready and waiting for an in-progress slot. Show a "Ready" badge so operators can distinguish planned cards from freshly promoted unplanned ones. Tasks still being planned surface the "planning" status badge above instead.
|
||||||
|
|||||||
@@ -1865,6 +1865,84 @@ describe("ListView", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("shows the Reviewing badge in the desktop table status cell while Plan Review runs", () => {
|
||||||
|
const tasks = [
|
||||||
|
createMockTask({
|
||||||
|
id: "FN-7831",
|
||||||
|
status: "planning",
|
||||||
|
enabledWorkflowSteps: ["plan-review"],
|
||||||
|
workflowStepResults: [
|
||||||
|
{
|
||||||
|
workflowStepId: "plan-review",
|
||||||
|
workflowStepName: "Plan Review",
|
||||||
|
status: "pending",
|
||||||
|
startedAt: "2026-07-11T12:00:00.000Z",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} as Partial<Task>),
|
||||||
|
];
|
||||||
|
|
||||||
|
renderListView({ tasks });
|
||||||
|
|
||||||
|
const row = screen.getByText("FN-7831").closest("tr");
|
||||||
|
expect(row).not.toBeNull();
|
||||||
|
expect(within(row as HTMLElement).getByText("Reviewing")).toBeInTheDocument();
|
||||||
|
expect(within(row as HTMLElement).getByText("planning")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows the Reviewing badge in grouped mobile cards while Plan Review runs", () => {
|
||||||
|
const matchMediaSpy = mockMobileViewport();
|
||||||
|
try {
|
||||||
|
const tasks = [
|
||||||
|
createMockTask({
|
||||||
|
id: "FN-7831",
|
||||||
|
status: "planning",
|
||||||
|
enabledWorkflowSteps: ["plan-review"],
|
||||||
|
workflowStepResults: [
|
||||||
|
{
|
||||||
|
workflowStepId: "plan-review",
|
||||||
|
workflowStepName: "Plan Review",
|
||||||
|
status: "pending",
|
||||||
|
startedAt: "2026-07-11T12:00:00.000Z",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} as Partial<Task>),
|
||||||
|
];
|
||||||
|
|
||||||
|
renderListView({ tasks });
|
||||||
|
|
||||||
|
const card = screen.getByText("FN-7831").closest(".list-card");
|
||||||
|
expect(card).not.toBeNull();
|
||||||
|
expect(within(card as HTMLElement).getByText("Reviewing")).toBeInTheDocument();
|
||||||
|
expect(within(card as HTMLElement).getByText("planning")).toBeInTheDocument();
|
||||||
|
} finally {
|
||||||
|
matchMediaSpy.mockRestore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not show the Reviewing badge after Plan Review completes", () => {
|
||||||
|
const tasks = [
|
||||||
|
createMockTask({
|
||||||
|
id: "FN-7831",
|
||||||
|
status: "planning",
|
||||||
|
enabledWorkflowSteps: ["plan-review"],
|
||||||
|
workflowStepResults: [
|
||||||
|
{
|
||||||
|
workflowStepId: "plan-review",
|
||||||
|
workflowStepName: "Plan Review",
|
||||||
|
status: "passed",
|
||||||
|
startedAt: "2026-07-11T12:00:00.000Z",
|
||||||
|
completedAt: "2026-07-11T12:01:00.000Z",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} as Partial<Task>),
|
||||||
|
];
|
||||||
|
|
||||||
|
renderListView({ tasks });
|
||||||
|
|
||||||
|
expect(screen.queryByText("Reviewing")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
it("renders paused tasks with dimmed styling", () => {
|
it("renders paused tasks with dimmed styling", () => {
|
||||||
const tasks = [createMockTask({ id: "FN-001", paused: true })];
|
const tasks = [createMockTask({ id: "FN-001", paused: true })];
|
||||||
|
|
||||||
|
|||||||
@@ -1986,6 +1986,37 @@ describe("TaskCard", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
{ name: "undefined results", workflowStepResults: undefined, shouldRender: false },
|
||||||
|
{ name: "pending but not started", workflowStepResults: [{ workflowStepId: "plan-review", workflowStepName: "Plan Review", status: "pending" }], shouldRender: false },
|
||||||
|
{ name: "running", workflowStepResults: [{ workflowStepId: "plan-review", workflowStepName: "Plan Review", status: "pending", startedAt: "2026-07-11T12:00:00.000Z" }], shouldRender: true },
|
||||||
|
{ name: "passed", workflowStepResults: [{ workflowStepId: "plan-review", workflowStepName: "Plan Review", status: "passed", startedAt: "2026-07-11T12:00:00.000Z", completedAt: "2026-07-11T12:01:00.000Z" }], shouldRender: false },
|
||||||
|
{ name: "failed", workflowStepResults: [{ workflowStepId: "plan-review", workflowStepName: "Plan Review", status: "failed", startedAt: "2026-07-11T12:00:00.000Z", completedAt: "2026-07-11T12:01:00.000Z" }], shouldRender: false },
|
||||||
|
{ name: "skipped", workflowStepResults: [{ workflowStepId: "plan-review", workflowStepName: "Plan Review", status: "skipped", startedAt: "2026-07-11T12:00:00.000Z", completedAt: "2026-07-11T12:01:00.000Z" }], shouldRender: false },
|
||||||
|
{ name: "advisory failure", workflowStepResults: [{ workflowStepId: "plan-review", workflowStepName: "Plan Review", status: "advisory_failure", startedAt: "2026-07-11T12:00:00.000Z", completedAt: "2026-07-11T12:01:00.000Z" }], shouldRender: false },
|
||||||
|
])("renders the Reviewing badge only while Plan Review is actively running: $name", ({ workflowStepResults, shouldRender }) => {
|
||||||
|
const { container } = render(
|
||||||
|
<TaskCard
|
||||||
|
task={makeTask({
|
||||||
|
id: "FN-7831",
|
||||||
|
column: "triage",
|
||||||
|
status: "planning",
|
||||||
|
enabledWorkflowSteps: ["plan-review"],
|
||||||
|
workflowStepResults: workflowStepResults as Task["workflowStepResults"],
|
||||||
|
})}
|
||||||
|
onOpenDetail={noop}
|
||||||
|
addToast={noop}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const badge = container.querySelector('[data-testid="card-reviewing-FN-7831"]');
|
||||||
|
expect(Boolean(badge)).toBe(shouldRender);
|
||||||
|
if (shouldRender) {
|
||||||
|
expect(badge).toHaveTextContent("Reviewing");
|
||||||
|
expect(screen.getByText("planning")).toBeDefined();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("renders the status badge after the card ID in DOM order", () => {
|
it("renders the status badge after the card ID in DOM order", () => {
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
<TaskCard
|
<TaskCard
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import type { Task } from "@fusion/core";
|
import type { Task } from "@fusion/core";
|
||||||
import { getUnifiedTaskProgress } from "../taskProgress";
|
import { getUnifiedTaskProgress, isPlanReviewRunning } from "../taskProgress";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
FNXC:WorkflowSteps 2026-06-25-00:00 — graph-native progress model (plan U3).
|
FNXC:WorkflowSteps 2026-06-25-00:00 — graph-native progress model (plan U3).
|
||||||
@@ -20,6 +20,20 @@ function makeTask(overrides: Partial<Pick<Task, "steps" | "enabledWorkflowSteps"
|
|||||||
} as Pick<Task, "steps" | "enabledWorkflowSteps" | "workflowStepResults">;
|
} as Pick<Task, "steps" | "enabledWorkflowSteps" | "workflowStepResults">;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
describe("isPlanReviewRunning", () => {
|
||||||
|
it.each([
|
||||||
|
{ name: "undefined results", task: { enabledWorkflowSteps: ["plan-review"], workflowStepResults: undefined }, expected: false },
|
||||||
|
{ name: "pending but not started", task: { enabledWorkflowSteps: ["plan-review"], workflowStepResults: [{ workflowStepId: "plan-review", workflowStepName: "Plan Review", status: "pending" }] }, expected: false },
|
||||||
|
{ name: "started and not completed", task: { enabledWorkflowSteps: ["plan-review"], workflowStepResults: [{ workflowStepId: "plan-review", workflowStepName: "Plan Review", status: "pending", startedAt: "2026-07-11T12:00:00.000Z" }] }, expected: true },
|
||||||
|
{ name: "passed", task: { enabledWorkflowSteps: ["plan-review"], workflowStepResults: [{ workflowStepId: "plan-review", workflowStepName: "Plan Review", status: "passed", startedAt: "2026-07-11T12:00:00.000Z", completedAt: "2026-07-11T12:01:00.000Z" }] }, expected: false },
|
||||||
|
{ name: "failed", task: { enabledWorkflowSteps: ["plan-review"], workflowStepResults: [{ workflowStepId: "plan-review", workflowStepName: "Plan Review", status: "failed", startedAt: "2026-07-11T12:00:00.000Z", completedAt: "2026-07-11T12:01:00.000Z" }] }, expected: false },
|
||||||
|
{ name: "skipped", task: { enabledWorkflowSteps: ["plan-review"], workflowStepResults: [{ workflowStepId: "plan-review", workflowStepName: "Plan Review", status: "skipped", startedAt: "2026-07-11T12:00:00.000Z", completedAt: "2026-07-11T12:01:00.000Z" }] }, expected: false },
|
||||||
|
{ name: "advisory failure", task: { enabledWorkflowSteps: ["plan-review"], workflowStepResults: [{ workflowStepId: "plan-review", workflowStepName: "Plan Review", status: "advisory_failure", startedAt: "2026-07-11T12:00:00.000Z", completedAt: "2026-07-11T12:01:00.000Z" }] }, expected: false },
|
||||||
|
])("returns $expected for $name", ({ task, expected }) => {
|
||||||
|
expect(isPlanReviewRunning(makeTask(task as Partial<Pick<Task, "steps" | "enabledWorkflowSteps" | "workflowStepResults">>))).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("getUnifiedTaskProgress", () => {
|
describe("getUnifiedTaskProgress", () => {
|
||||||
it("resolves workflow step names from result.workflowStepName without a lookup", () => {
|
it("resolves workflow step names from result.workflowStepName without a lookup", () => {
|
||||||
const progress = getUnifiedTaskProgress(
|
const progress = getUnifiedTaskProgress(
|
||||||
|
|||||||
@@ -139,3 +139,13 @@ export function getUnifiedTaskProgress(
|
|||||||
|
|
||||||
return { total, completed, items };
|
return { total, completed, items };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
FNXC:TaskCardPlanReviewBadge 2026-07-11-12:00:
|
||||||
|
FN-7831 requires task cards and list rows to show a distinct "Reviewing" badge only while the optional `plan-review` workflow step is actively running. Reuse the unified progress item status so every board surface follows the same startedAt-without-completedAt semantics as the progress list.
|
||||||
|
*/
|
||||||
|
export function isPlanReviewRunning(task: Pick<Task, "steps" | "enabledWorkflowSteps" | "workflowStepResults">): boolean {
|
||||||
|
return getUnifiedTaskProgress(task).items.some(
|
||||||
|
(item) => item.id === "workflow-plan-review" && item.status === "running",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user