feat(FN-3231): preserve merge-active state on verification bounce and board

This merge lands FN-3231 across two steps: it preserves a merge-active fix when verification bounces occur (step 1) and ensures the fix is retained during board routing transitions (step 2). Changes span the dashboard Board routing logic and the engine executor, with corresponding test coverage adde

Fusion-Task-Id: FN-3231
This commit is contained in:
Fusion
2026-05-04 06:55:59 -07:00
committed by gsxdsm
parent 004a0ad904
commit 1bd32a542d
14 changed files with 172 additions and 38 deletions

View File

@@ -403,6 +403,28 @@ describe("Board", () => {
expect(inReviewTasks.map((task) => task.id)).toEqual(["FN-020", "FN-021"]);
});
it("pins merging-fix tasks to top of in-review even when newer non-merging tasks exist", () => {
const tasks: Task[] = [
createTask({
id: "FN-060",
column: "in-review",
status: "merging-fix",
columnMovedAt: "2024-01-01T10:00:00.000Z",
}),
createTask({
id: "FN-061",
column: "in-review",
status: "review-ready",
columnMovedAt: "2024-01-01T13:00:00.000Z",
}),
];
renderBoard({ tasks });
const inReviewTasks = JSON.parse(screen.getByTestId("column-in-review").getAttribute("data-tasks") || "[]") as Task[];
expect(inReviewTasks.map((task) => task.id)).toEqual(["FN-060", "FN-061"]);
});
it("sorts multiple merging tasks by priority then task ID within the pinned group", () => {
const tasks: Task[] = [
createTask({

View File

@@ -620,12 +620,15 @@ describe("ListView", () => {
expect(row?.className).toContain("paused");
});
it("renders agent-active tasks with glow styling", () => {
it.each([
{ status: "executing", column: "in-progress" as const, label: "executing" },
{ status: "merging-fix", column: "in-review" as const, label: "Merging fixes…" },
])("renders agent-active tasks with glow styling for $status", ({ status, column, label }) => {
const tasks = [
createMockTask({
id: "FN-001",
status: "executing",
column: "in-progress",
status,
column,
}),
];
@@ -633,6 +636,7 @@ describe("ListView", () => {
const row = screen.getByText("FN-001").closest("tr");
expect(row?.className).toContain("agent-active");
expect(screen.getByText(label)).toBeInTheDocument();
});
it("does not render agent-active when globalPaused is true", () => {
@@ -2845,15 +2849,18 @@ describe("ListView - Bulk Selection", () => {
expect((screen.getByLabelText("Select FN-002") as HTMLInputElement).checked).toBe(true);
});
it("applies agent-active class to mobile cards when task is in-progress and not paused/failed", () => {
it.each([
{ status: "executing", column: "in-progress" as const },
{ status: "merging-fix", column: "in-review" as const },
])("applies agent-active class to mobile cards for active states (%s)", ({ status, column }) => {
mockMobileViewport();
const { container } = renderListView({
tasks: [
createMockTask({
id: "FN-001",
status: "executing",
column: "in-progress",
status,
column,
}),
],
globalPaused: false,

View File

@@ -224,8 +224,8 @@ describe("RoutingTab", () => {
expect(screen.getByText("Node override cannot be changed while the task is active.")).toBeInTheDocument();
});
it("disables node selector for active task statuses", async () => {
render(<RoutingTab task={makeTask({ column: "todo", status: "executing" })} settings={makeSettings()} addToast={addToast} />);
it.each(["executing", "merging-fix"] as const)("disables node selector for active task status %s", async (status) => {
render(<RoutingTab task={makeTask({ column: "todo", status })} settings={makeSettings()} addToast={addToast} />);
const selector = await screen.findByLabelText("Select execution node");
expect(selector).toBeDisabled();

View File

@@ -70,6 +70,20 @@ describe("TaskCard", () => {
expect(screen.getByText("executing")).toBeDefined();
});
it("renders merge-remediation status as merge-active for in-review tasks", () => {
const { container } = render(
<TaskCard
task={makeTask({ column: "in-review", status: "merging-fix" })}
onOpenDetail={noop}
addToast={noop}
/>,
);
expect(screen.getByText("Merging fixes…")).toBeDefined();
const badge = container.querySelector(".card-status-badge");
expect(badge?.className).toContain("pulsing");
});
it("renders the status badge after the card ID in DOM order", () => {
const { container } = render(
<TaskCard
@@ -856,7 +870,7 @@ describe("TaskCard", () => {
expect(container.querySelector(".card-time-indicator")?.getAttribute("title")).toBe("Execution time 35m");
});
it("shows live merge elapsed in timer chip while task.status is merging", () => {
it.each(["merging", "merging-fix"] as const)("shows live merge elapsed in timer chip while task.status is %s", (status) => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-04-25T13:45:00.000Z"));
@@ -865,7 +879,7 @@ describe("TaskCard", () => {
<TaskCard
task={makeTask({
column: "in-review",
status: "merging",
status,
executionStartedAt: "2026-04-25T13:00:00.000Z",
updatedAt: "2026-04-25T13:44:30.000Z",
workflowStepResults: [