feat(FN-4462): complete Step 1 — per-code stall copy and counter
Fusion-Task-Id: FN-4462 Fusion-Task-Lineage: 15fc09f5-1363-429e-b131-9c9fdabee532
This commit is contained in:
@@ -4,23 +4,75 @@ import { getInReviewStallCopy, shouldShowInReviewStallBadge } from "../inReviewS
|
||||
|
||||
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) => {
|
||||
["merge-blocker", "Merge blocked"],
|
||||
["transient-merge-status-no-owner", "Merge stalled"],
|
||||
["merge-retries-exhausted", "Retries exhausted"],
|
||||
["no-worktree-no-merge-confirmed", "No worktree"],
|
||||
] as const)("returns populated copy for %s", (code, badgeLabel) => {
|
||||
const copy = getInReviewStallCopy({
|
||||
code,
|
||||
reason: "reason",
|
||||
observedAt: "2026-05-13T00:00:00.000Z",
|
||||
});
|
||||
|
||||
expect(copy.badgeLabel).toBe("Stall");
|
||||
expect(copy.badgeLabel).toBe(badgeLabel);
|
||||
expect(copy.headline.length).toBeGreaterThan(0);
|
||||
expect(copy.description.length).toBeGreaterThan(0);
|
||||
expect(copy.suggestedAction.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("renders merge retry counter when retries are supplied", () => {
|
||||
const copy = getInReviewStallCopy(
|
||||
{
|
||||
code: "merge-retries-exhausted",
|
||||
reason: "reason",
|
||||
observedAt: "2026-05-13T00:00:00.000Z",
|
||||
},
|
||||
{ mergeRetries: 3 },
|
||||
);
|
||||
|
||||
expect(copy.counter).toBe("3/3");
|
||||
});
|
||||
|
||||
it("renders merge retry counter above max when retries exceed max", () => {
|
||||
const copy = getInReviewStallCopy(
|
||||
{
|
||||
code: "merge-retries-exhausted",
|
||||
reason: "reason",
|
||||
observedAt: "2026-05-13T00:00:00.000Z",
|
||||
},
|
||||
{ mergeRetries: 5 },
|
||||
);
|
||||
|
||||
expect(copy.counter).toBe("5/3");
|
||||
});
|
||||
|
||||
it("omits merge retry counter without retry context", () => {
|
||||
const copy = getInReviewStallCopy({
|
||||
code: "merge-retries-exhausted",
|
||||
reason: "reason",
|
||||
observedAt: "2026-05-13T00:00:00.000Z",
|
||||
});
|
||||
|
||||
expect(copy.counter).toBeUndefined();
|
||||
});
|
||||
|
||||
it.each(["merge-blocker", "transient-merge-status-no-owner", "no-worktree-no-merge-confirmed"] as const)(
|
||||
"does not render counter for non-retry stall code %s",
|
||||
(code) => {
|
||||
const copy = getInReviewStallCopy(
|
||||
{
|
||||
code,
|
||||
reason: "reason",
|
||||
observedAt: "2026-05-13T00:00:00.000Z",
|
||||
},
|
||||
{ mergeRetries: 99 },
|
||||
);
|
||||
|
||||
expect(copy.counter).toBeUndefined();
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
{ column: "in-review", paused: false, inReviewStall: undefined },
|
||||
{
|
||||
@@ -57,6 +109,6 @@ describe("inReviewStallCopy", () => {
|
||||
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");
|
||||
expect(copy.badgeLabel).toBe("In-review stall");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,16 +1,24 @@
|
||||
import type { InReviewStallCode, InReviewStallSignal, Task } from "@fusion/core";
|
||||
|
||||
import { MAX_AUTO_MERGE_RETRIES } from "../hooks/useBlockerFanout";
|
||||
|
||||
export interface InReviewStallCopy {
|
||||
badgeLabel: string;
|
||||
counter?: string;
|
||||
headline: string;
|
||||
description: string;
|
||||
suggestedAction: string;
|
||||
code: InReviewStallCode;
|
||||
}
|
||||
|
||||
const BADGE_LABEL = "Stall";
|
||||
const BADGE_LABEL_BY_CODE: Record<InReviewStallCode, string> = {
|
||||
"merge-blocker": "Merge blocked",
|
||||
"transient-merge-status-no-owner": "Merge stalled",
|
||||
"merge-retries-exhausted": "Retries exhausted",
|
||||
"no-worktree-no-merge-confirmed": "No worktree",
|
||||
};
|
||||
|
||||
const COPY_BY_CODE: Record<InReviewStallCode, Omit<InReviewStallCopy, "badgeLabel" | "code">> = {
|
||||
const COPY_BY_CODE: Record<InReviewStallCode, Omit<InReviewStallCopy, "badgeLabel" | "counter" | "code">> = {
|
||||
"merge-blocker": {
|
||||
headline: "Merge blocked by a pre-merge check",
|
||||
description:
|
||||
@@ -43,7 +51,7 @@ function defaultCopy(signal: InReviewStallSignal): InReviewStallCopy {
|
||||
console.warn(`Unhandled inReviewStall code in dashboard copy map: ${signal.code}`);
|
||||
}
|
||||
return {
|
||||
badgeLabel: BADGE_LABEL,
|
||||
badgeLabel: "In-review stall",
|
||||
code: signal.code,
|
||||
headline: "In-review stall surfaced",
|
||||
description: signal.reason,
|
||||
@@ -51,14 +59,26 @@ function defaultCopy(signal: InReviewStallSignal): InReviewStallCopy {
|
||||
};
|
||||
}
|
||||
|
||||
export function getInReviewStallCopy(signal: InReviewStallSignal): InReviewStallCopy {
|
||||
export function getInReviewStallCopy(
|
||||
signal: InReviewStallSignal,
|
||||
options?: { mergeRetries?: number | null; maxAutoMergeRetries?: number },
|
||||
): InReviewStallCopy {
|
||||
const mapped = COPY_BY_CODE[signal.code];
|
||||
if (!mapped) {
|
||||
return defaultCopy(signal);
|
||||
}
|
||||
|
||||
const maxAutoMergeRetries = options?.maxAutoMergeRetries ?? MAX_AUTO_MERGE_RETRIES;
|
||||
const mergeRetries = options?.mergeRetries;
|
||||
const counter =
|
||||
signal.code === "merge-retries-exhausted" && Number.isFinite(mergeRetries) && mergeRetries != null && mergeRetries >= 0
|
||||
? `${Math.max(mergeRetries, maxAutoMergeRetries)}/${maxAutoMergeRetries}`
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
badgeLabel: BADGE_LABEL,
|
||||
badgeLabel: BADGE_LABEL_BY_CODE[signal.code],
|
||||
code: signal.code,
|
||||
counter,
|
||||
...mapped,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user