feat(FN-4288): complete Step 1 — add in-review stall copy module
Fusion-Task-Id: FN-4288 Fusion-Task-Lineage: 09c6efc0-ea0b-443a-ab45-a89188fcf888
This commit is contained in:
@@ -0,0 +1,62 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import { getInReviewStallCopy, shouldShowInReviewStallBadge } from "../inReviewStallCopy";
|
||||||
|
|
||||||
|
describe("inReviewStallCopy", () => {
|
||||||
|
it.each([
|
||||||
|
"merge-blocker",
|
||||||
|
"transient-merge-status-no-owner",
|
||||||
|
"merge-retries-exhausted",
|
||||||
|
"no-worktree-no-merge-confirmed",
|
||||||
|
] as const)("returns populated copy for %s", (code) => {
|
||||||
|
const copy = getInReviewStallCopy({
|
||||||
|
code,
|
||||||
|
reason: "reason",
|
||||||
|
observedAt: "2026-05-13T00:00:00.000Z",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(copy.badgeLabel).toBe("Stall");
|
||||||
|
expect(copy.headline.length).toBeGreaterThan(0);
|
||||||
|
expect(copy.description.length).toBeGreaterThan(0);
|
||||||
|
expect(copy.suggestedAction.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
{ column: "in-review", paused: false, inReviewStall: undefined },
|
||||||
|
{
|
||||||
|
column: "in-review",
|
||||||
|
paused: true,
|
||||||
|
inReviewStall: { code: "merge-blocker", reason: "r", observedAt: "2026-05-13T00:00:00.000Z" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
column: "in-progress",
|
||||||
|
paused: false,
|
||||||
|
inReviewStall: { code: "merge-blocker", reason: "r", observedAt: "2026-05-13T00:00:00.000Z" },
|
||||||
|
},
|
||||||
|
] as const)("hides badge for non-canonical visibility cases", (task) => {
|
||||||
|
expect(shouldShowInReviewStallBadge(task)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows badge only for in-review non-paused task with signal", () => {
|
||||||
|
expect(
|
||||||
|
shouldShowInReviewStallBadge({
|
||||||
|
column: "in-review",
|
||||||
|
paused: false,
|
||||||
|
inReviewStall: { code: "merge-blocker", reason: "r", observedAt: "2026-05-13T00:00:00.000Z" },
|
||||||
|
}),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to defensive default for unknown codes", () => {
|
||||||
|
const copy = getInReviewStallCopy({
|
||||||
|
code: "future-code" as never,
|
||||||
|
reason: "future reason",
|
||||||
|
observedAt: "2026-05-13T00:00:00.000Z",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(copy.headline).toBe("In-review stall surfaced");
|
||||||
|
expect(copy.description).toBe("future reason");
|
||||||
|
expect(copy.suggestedAction).toBe("Open the activity log for details.");
|
||||||
|
expect(copy.badgeLabel).toBe("Stall");
|
||||||
|
});
|
||||||
|
});
|
||||||
68
packages/dashboard/app/utils/inReviewStallCopy.ts
Normal file
68
packages/dashboard/app/utils/inReviewStallCopy.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import type { InReviewStallCode, InReviewStallSignal, Task } from "@fusion/core";
|
||||||
|
|
||||||
|
export interface InReviewStallCopy {
|
||||||
|
badgeLabel: string;
|
||||||
|
headline: string;
|
||||||
|
description: string;
|
||||||
|
suggestedAction: string;
|
||||||
|
code: InReviewStallCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BADGE_LABEL = "Stall";
|
||||||
|
|
||||||
|
const COPY_BY_CODE: Record<InReviewStallCode, Omit<InReviewStallCopy, "badgeLabel" | "code">> = {
|
||||||
|
"merge-blocker": {
|
||||||
|
headline: "Merge blocked by a pre-merge check",
|
||||||
|
description:
|
||||||
|
"A workflow step or merge precondition is reporting a blocker. The task is waiting for that check to pass before it can finalize.",
|
||||||
|
suggestedAction: "Open the Review tab to see which step is blocking, then fix the failure or override the step.",
|
||||||
|
},
|
||||||
|
"transient-merge-status-no-owner": {
|
||||||
|
headline: "Stuck in a transient merge state with no active merger",
|
||||||
|
description:
|
||||||
|
"The task is parked in a merging/merging-pr/merging-fix status but no merger process owns it. Self-healing will retry, but if this repeats the merge worker may need attention.",
|
||||||
|
suggestedAction: "Wait one self-healing cycle; if it persists, inspect engine logs for crashed merger runs.",
|
||||||
|
},
|
||||||
|
"merge-retries-exhausted": {
|
||||||
|
headline: "Auto-merge retries exhausted",
|
||||||
|
description: "The merger hit its retry ceiling without confirming a merge. The task will not be re-enqueued automatically.",
|
||||||
|
suggestedAction:
|
||||||
|
"Resolve the underlying merge problem manually and re-run the merge from the Review tab, or move the task back to in-progress.",
|
||||||
|
},
|
||||||
|
"no-worktree-no-merge-confirmed": {
|
||||||
|
headline: "No worktree on disk and merge not confirmed",
|
||||||
|
description:
|
||||||
|
"The task's working tree is gone but the merge was never confirmed. Either the worktree was removed prematurely or the merge metadata is incomplete.",
|
||||||
|
suggestedAction:
|
||||||
|
"Check the Changes tab and Git history; if the work landed, mark the merge confirmed, otherwise re-create the worktree.",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function defaultCopy(signal: InReviewStallSignal): InReviewStallCopy {
|
||||||
|
if (process.env.NODE_ENV !== "production") {
|
||||||
|
console.warn(`Unhandled inReviewStall code in dashboard copy map: ${signal.code}`);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
badgeLabel: BADGE_LABEL,
|
||||||
|
code: signal.code,
|
||||||
|
headline: "In-review stall surfaced",
|
||||||
|
description: signal.reason,
|
||||||
|
suggestedAction: "Open the activity log for details.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getInReviewStallCopy(signal: InReviewStallSignal): InReviewStallCopy {
|
||||||
|
const mapped = COPY_BY_CODE[signal.code];
|
||||||
|
if (!mapped) {
|
||||||
|
return defaultCopy(signal);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
badgeLabel: BADGE_LABEL,
|
||||||
|
code: signal.code,
|
||||||
|
...mapped,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function shouldShowInReviewStallBadge(task: Pick<Task, "column" | "paused" | "inReviewStall">): boolean {
|
||||||
|
return task.column === "in-review" && task.paused !== true && task.inReviewStall != null;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user