Files
fusion/packages/dashboard/app/utils/__tests__/taskRevert.test.ts
gsxdsm bf173dad7c FN-8696: keep reverted tasks out of Done
Keep reverted tasks out of completed views and give every task-detail host a revision recovery path.

- Exclude reverted tasks from complete columns and group them in dedicated recovery lists.
- Provide Delete and Revise actions in cards, lists, docks, modals, and popped-out details.
- Add localized copy, documentation, regression coverage, and a patch changeset.

Files changed:
 .changeset/fn-8696-reverted-task-resolution.md     |  7 ++++
 docs/dashboard-guide.md                            |  4 ++
 packages/dashboard/app/App.tsx                     |  2 +
 packages/dashboard/app/components/AppModals.tsx    |  2 +
 packages/dashboard/app/components/Board.tsx        | 42 +++++++++++++++++++-
 packages/dashboard/app/components/DockTaskList.tsx | 21 +++++++++-
 packages/dashboard/app/components/ListView.tsx     | 18 +++++++++
 packages/dashboard/app/components/TaskCard.css     |  4 ++
 packages/dashboard/app/components/TaskCard.tsx     |  9 +++++
 .../dashboard/app/components/TaskDetailModal.tsx   | 18 ++++++++-
 .../app/components/__tests__/Board.test.tsx        | 46 ++++++++++++++++++++++
 .../app/components/__tests__/DockTaskList.test.tsx | 29 +++++++++++++-
 .../app/components/dashboard/MainContent.tsx       |  4 ++
 .../app/components/overflowViewRegistry.tsx        |  3 ++
 .../app/components/useRightDockController.tsx      |  9 +++++
 .../app/utils/__tests__/taskRevert.test.ts         | 16 +++++++-
 packages/dashboard/app/utils/taskRevert.ts         | 18 +++++++++
 packages/i18n/locales/en/app.json                  |  3 ++
 18 files changed, 249 insertions(+), 6 deletions(-)

Fusion-Task-Id: FN-8696
Fusion-Task-Lineage: 7c9550fb-ef8e-4422-8ec1-0de1665413a3
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-08-01 13:30:18 -07:00

84 lines
4.0 KiB
TypeScript

import { describe, expect, it } from "vitest";
import type { Task } from "@fusion/core";
import { isTaskReverted, findOpenUndoTaskForSource, partitionRevertedTasks } from "../taskRevert";
describe("isTaskReverted", () => {
it.each([
[undefined, false],
[{}, false],
[{ revertedAt: "" }, false],
[{ revertedAt: " " }, false],
[{ revertedAt: 123 }, false],
[{ revertedAt: true }, false],
[{ revertedAt: "2026-07-16T00:00:00.000Z" }, true],
] as const)("returns %s for revertedAt metadata", (sourceMetadata, expected) => {
expect(isTaskReverted(sourceMetadata as Task["sourceMetadata"] | undefined)).toBe(expected);
});
});
describe("partitionRevertedTasks", () => {
const task = (id: string, revertedAt?: unknown): Task => ({
id, column: "done", title: id, description: "", createdAt: "", updatedAt: "", dependencies: [], steps: [],
sourceMetadata: revertedAt === undefined ? {} : { revertedAt },
} as unknown as Task);
it("keeps invalid markers normal and returns valid markers once", () => {
const reverted = task("KB-2", "2026-08-01T00:00:00.000Z");
const result = partitionRevertedTasks([task("KB-1", " "), reverted, reverted, task("KB-3", 42)]);
expect(result.normal.map(({ id }) => id)).toEqual(["KB-1", "KB-3"]);
expect(result.reverted.map(({ id }) => id)).toEqual(["KB-2"]);
});
});
/*
FNXC:WorkflowResolvedColumns 2026-07-31-23:30:
THE UNDO-TASK LOOKUP CLASSIFIED A NEIGHBOUR'S COLUMN BY ID.
`findOpenUndoTaskForSource` skips candidates that are finished, so a done/archived prior undo attempt
never renders as an active "Undo task" link. Keyed on `done`/`archived`, a board that renames those
lanes matched neither: a FINISHED undo task kept rendering as an open one, which is the stale
affordance the function's own header says it exists to prevent.
The flags are PER-CANDIDATE, keyed by task id. That is what makes this correct and what two earlier
notes said was unavailable — `TaskDetailModal` has had `columnFlagsByTaskId` as a prop all along and
already uses it this way for the near-duplicate canonical.
Both directions are asserted, and the negative is the load-bearing one: the map is fail-soft, so a
candidate it does not cover must still be treated as OPEN rather than silently skipped. A conversion
that skipped unknown candidates would hide live undo links.
*/
describe("findOpenUndoTaskForSource resolves each candidate's own lanes", () => {
const candidate = (id: string, column: string, createdAt: string): Task => ({
id, column, title: id, description: "", createdAt, updatedAt: createdAt,
dependencies: [], steps: [], sourceMetadata: { revertOf: "KB-SRC" },
} as unknown as Task);
it("skips an undo task resting in a RENAMED complete lane", () => {
const tasks = [candidate("KB-UNDO", "shipped", "2026-06-01T00:00:00.000Z")];
const flags = new Map([["KB-UNDO", { complete: true }]]);
expect(findOpenUndoTaskForSource(tasks, "KB-SRC", flags as never)).toBeUndefined();
});
it("still returns an undo task resting in a live lane on that same board", () => {
const tasks = [candidate("KB-UNDO", "building", "2026-06-01T00:00:00.000Z")];
const flags = new Map([["KB-UNDO", { countsTowardWip: true }]]);
expect(findOpenUndoTaskForSource(tasks, "KB-SRC", flags as never)?.id).toBe("KB-UNDO");
});
it("treats a candidate the map does not cover as OPEN, not skipped", () => {
/* Fail-soft in the safe direction: an unknown candidate keeps its link rather than losing it. */
const tasks = [candidate("KB-UNDO", "building", "2026-06-01T00:00:00.000Z")];
expect(findOpenUndoTaskForSource(tasks, "KB-SRC", new Map() as never)?.id).toBe("KB-UNDO");
});
it("still skips the legacy ids when no flags are supplied at all", () => {
/* CONTROL: the parameter is optional, so an unwired caller behaves exactly as before. */
const tasks = [candidate("KB-UNDO", "done", "2026-06-01T00:00:00.000Z")];
expect(findOpenUndoTaskForSource(tasks, "KB-SRC")).toBeUndefined();
});
});