Files
fusion/packages/dashboard/app/utils/__tests__/taskStatusBadgeLabel.test.ts
gsxdsm 99c9f14ee0 feat: run Plan Review in the planning lane with a Plan Review badge (#2462)
## What

Plan Review, planning, and the replan loop move from the implementation
column into the **planning lane** (`todo`), so a task under
specification never holds a WIP slot. The card crosses into
`in-progress` exactly once, at `parse`, released by the scheduler.

Operators also finally see a **Plan Review** badge while the gate runs —
it was previously invisible on the default workflow.

## The part that made it possible

Moving the node is ten lines. It was attempted three times and reverted
each time, because a graph run with no durable continuation replayed
from `start` and dragged an in-progress card *backward* out of the WIP
column, firing `abort-on-exit` and stranding it in a pre-WIP column with
no releaser.

So this PR adds the graph **entry contract** —
`resolveColumnResumeNode`:

| Card is in | Resumes at |
|---|---|
| `triage` | `start` |
| `todo` | `plan` |
| `in-progress` | `parse` — never re-plans, never moves backward |
| `in-review` | first review node — gates are not skipped |

`ir.columns` is ordered and that order is the lifecycle order; rework
and failure edges are excluded so the entry point is always the main
path. The proof it's the right fix: **`executor-task-done-invariant`
passes unmodified** after failing every previous attempt.

## Also in here

- **Release gate narrowed twice.** `isUnplannedForExecution` applies its
pre-release plan-review gate only when the node's column equals the
card's column *and* the group is enabled for the task. The enablement
check fixes a real deadlock — a task with Plan Review toggled off was
held forever waiting for evidence nothing would ever write.
- **Badge cleanup.** Gate badge reads "Plan Review" instead of the
ambiguous "Reviewing" and no longer hides behind a lane restriction; the
status badge stops duplicating it; `planning` renders as "Planning"
instead of the raw engine token.
- **Coding (Ideas)** renames its planner column to "Planning" (id `todo`
unchanged) and loses its private planning-node re-home — the graph it
clones is already plan-in-place.
- **New sweep** `reconcileUndeclaredTaskColumns` re-homes a row whose
column its workflow no longer declares. Written for a follow-up, kept
because it makes any column edit survivable.

## Test changes

Scheduler and release fixtures now model a card whose Plan Review passed
— the state every real card is in when the capacity sweep sees it. A
held unreviewed card is the gate working, and that path stays owned by
`pre-release-plan-review.test.ts`.

New `workflow-graph-entry-contract.test.ts` covers the invariant at
every lifecycle position, plus the gap-column and remediation-node
cases.

## Verification

Gate 299 + 70 + 10, dashboard badge suites 672, engine
workflow/entry/executor suites 147, core 122. Lint and typecheck clean.
Full engine suite sits at the pre-existing baseline (notifier /
plugin-runner / notification-service, untouched by this).

## Follow-up

Removing the Todo column entirely is a separate ~207-site
lifecycle-vocabulary refactor — planned in
`docs/plans/2026-07-26-001-refactor-workflow-owned-lifecycle-plan.md`
(companion docs PR).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Plan Review now runs in the Planning lane before implementation
begins.
* Cards resume from their current workflow column without replaying
earlier steps.
* Added automatic recovery for cards stranded in outdated workflow
columns.
* **Improvements**
  * Renamed the Coding (Ideas) planner column to “Planning.”
* Refined Plan Review gating to respect enabled settings and the card’s
current column.
* Updated planning and Plan Review badges for clearer, consistent labels
across cards and lists.
* **Bug Fixes**
* Improved workflow transitions and release behavior around planning,
review, and execution.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 23:42:46 -07:00

76 lines
2.9 KiB
TypeScript

import { describe, expect, it } from "vitest";
import type { TFunction } from "i18next";
import { getTaskStatusBadgeLabel, hasTaskStatusBadge } 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("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("");
});
});