feat(FN-3596): unify board/list task ordering, split TaskDetailModal test m

The merge introduces a major TaskDetailModal test refactor (splitting a 6.7K-line monolith into five focused suites), significant merger improvements including autostash race-rescue, deduplication, and advisory logging for destructive operations, a new TUI narrow-mode log-split feature for the dashb

Fusion-Task-Id: FN-3596
This commit is contained in:
Fusion
2026-05-06 18:31:40 -07:00
committed by gsxdsm
parent 921c588fdd
commit 705e5cc1d3
9 changed files with 248 additions and 133 deletions

View File

@@ -6,6 +6,8 @@ import {
isTaskPriority,
normalizeTaskPriority,
sortTasksByPriorityThenAgeAndId,
compareTaskIdNumeric,
sortTasksForDisplayColumn,
} from "../task-priority.js";
import {
DEFAULT_TASK_PRIORITY,
@@ -55,9 +57,42 @@ describe("task-priority", () => {
expect(compareTasksByPriorityThenAgeAndId(tasks[0], tasks[1])).toBeGreaterThan(0);
});
it("compares numeric IDs with locale fallback", () => {
expect(compareTaskIdNumeric("FN-2", "FN-10")).toBeLessThan(0);
expect(compareTaskIdNumeric("TASK-B", "TASK-A")).toBeGreaterThan(0);
});
it("applies board/list default ordering semantics by column", () => {
const base = {
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
columnMovedAt: "2026-01-01T00:00:00.000Z",
};
const todoSorted = sortTasksForDisplayColumn([
{ ...base, id: "FN-003", column: "todo", priority: "low" as TaskPriority, createdAt: "2026-01-01T00:00:00.000Z" },
{ ...base, id: "FN-001", column: "todo", priority: "urgent" as TaskPriority, createdAt: "2026-01-02T00:00:00.000Z" },
{ ...base, id: "FN-002", column: "todo", priority: "high" as TaskPriority, createdAt: "2026-01-01T12:00:00.000Z" },
], "todo");
expect(todoSorted.map((task) => task.id)).toEqual(["FN-001", "FN-002", "FN-003"]);
const inReviewSorted = sortTasksForDisplayColumn([
{ ...base, id: "FN-010", column: "in-review", status: "review-ready", priority: "urgent" as TaskPriority },
{ ...base, id: "FN-011", column: "in-review", status: "merging-fix", priority: "high" as TaskPriority },
], "in-review");
expect(inReviewSorted.map((task) => task.id)).toEqual(["FN-011", "FN-010"]);
const doneSorted = sortTasksForDisplayColumn([
{ ...base, id: "FN-020", column: "done", priority: "urgent" as TaskPriority, columnMovedAt: "2026-01-01T08:00:00.000Z" },
{ ...base, id: "FN-021", column: "done", priority: "low" as TaskPriority, columnMovedAt: "2026-01-01T09:00:00.000Z" },
], "done");
expect(doneSorted.map((task) => task.id)).toEqual(["FN-021", "FN-020"]);
});
it("re-exports priority helpers from the core index", () => {
expect(core.TASK_PRIORITIES).toEqual(TASK_PRIORITIES);
expect(core.DEFAULT_TASK_PRIORITY).toBe("normal");
expect(core.normalizeTaskPriority("bogus")).toBe(DEFAULT_TASK_PRIORITY);
expect(typeof core.sortTasksForDisplayColumn).toBe("function");
});
});

View File

@@ -309,8 +309,10 @@ export {
compareTaskPriority,
compareTasksByPriorityThenAgeAndId,
sortTasksByPriorityThenAgeAndId,
compareTaskIdNumeric,
sortTasksForDisplayColumn,
} from "./task-priority.js";
export type { TaskPrioritySortable } from "./task-priority.js";
export type { TaskPrioritySortable, TaskColumnSortable } from "./task-priority.js";
export {
mapFeatureToTaskHandoff,
mapRoadmapToMissionHandoff,

View File

@@ -7,6 +7,13 @@ export interface TaskPrioritySortable {
priority?: TaskPriority | null;
}
export interface TaskColumnSortable extends TaskPrioritySortable {
column: string;
status?: string | null;
columnMovedAt?: string;
updatedAt?: string;
}
const PRIORITY_RANK: Record<TaskPriority, number> = {
low: 0,
normal: 1,
@@ -40,7 +47,7 @@ export function compareTaskPriority(a: unknown, b: unknown): number {
return getTaskPriorityRank(b) - getTaskPriorityRank(a);
}
function compareTaskId(a: string, b: string): number {
export function compareTaskIdNumeric(a: string, b: string): number {
const aNum = Number.parseInt(a.slice(a.lastIndexOf("-") + 1), 10);
const bNum = Number.parseInt(b.slice(b.lastIndexOf("-") + 1), 10);
@@ -65,7 +72,7 @@ export function compareTasksByPriorityThenAgeAndId<T extends TaskPrioritySortabl
return a.createdAt.localeCompare(b.createdAt);
}
return compareTaskId(a.id, b.id);
return compareTaskIdNumeric(a.id, b.id);
}
/**
@@ -76,3 +83,47 @@ export function sortTasksByPriorityThenAgeAndId<T extends TaskPrioritySortable>(
): T[] {
return [...tasks].sort(compareTasksByPriorityThenAgeAndId);
}
function getDoneSortTimestamp(task: TaskColumnSortable): number {
const timestamp = task.columnMovedAt ?? task.updatedAt ?? task.createdAt;
const parsed = Date.parse(timestamp);
return Number.isFinite(parsed) ? parsed : 0;
}
function isMergeActiveStatus(status: string | null | undefined): boolean {
return status === "merging" || status === "merging-pr" || status === "merging-fix";
}
/**
* Column-aware default ordering shared by board and list surfaces.
*/
export function sortTasksForDisplayColumn<T extends TaskColumnSortable>(tasks: readonly T[], column: string): T[] {
if (column === "todo") {
return sortTasksByPriorityThenAgeAndId(tasks);
}
return [...tasks].sort((a, b) => {
if (column === "done") {
const timestampCmp = getDoneSortTimestamp(b) - getDoneSortTimestamp(a);
if (timestampCmp !== 0) {
return timestampCmp;
}
return compareTaskIdNumeric(a.id, b.id);
}
if (column === "in-review") {
const aIsMerging = isMergeActiveStatus(a.status);
const bIsMerging = isMergeActiveStatus(b.status);
if (aIsMerging !== bIsMerging) {
return aIsMerging ? -1 : 1;
}
}
const priorityCmp = compareTaskPriority(a.priority, b.priority);
if (priorityCmp !== 0) {
return priorityCmp;
}
return compareTaskIdNumeric(a.id, b.id);
});
}