Fail fast on non-retryable provider failures in the in-review stall recovery flow. - classify failed-task provider errors as non-retryable, retryable, or unknown in core stall detection - surface terminal provider errors with dedicated dashboard copy and exported stall signal metadata - pause in-review tasks immediately on non-retryable provider failures and record a dedicated run-audit mutation - add regression coverage for provider error classification, self-healing disposition, and dashboard badge behavior Files changed: .../core/src/__tests__/in-review-stall.test.ts | 70 ++++++++++ packages/core/src/in-review-stall.ts | 53 ++++++- packages/core/src/index.ts | 4 +- .../app/utils/__tests__/inReviewStallCopy.test.ts | 5 +- packages/dashboard/app/utils/inReviewStallCopy.ts | 8 ++ .../in-review-stall-deadlock-disposition.test.ts | 153 +++++++++++++++++++++ packages/engine/src/run-audit.ts | 1 + packages/engine/src/self-healing.ts | 30 +++- 8 files changed, 320 insertions(+), 4 deletions(-) Fusion-Task-Id: FN-6113 Fusion-Task-Lineage: 5e89adb4-6b59-4deb-8811-5c75d6fa19cc
142 lines
4.5 KiB
TypeScript
142 lines
4.5 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import { getInReviewStallCopy, getInReviewStallDeadlockCopy, shouldShowInReviewStallBadge } from "../inReviewStallCopy";
|
|
|
|
describe("inReviewStallCopy", () => {
|
|
it.each([
|
|
["merge-blocker", "Merge blocked"],
|
|
["transient-merge-status-no-owner", "Merge stalled"],
|
|
["merge-retries-exhausted", "Retries exhausted"],
|
|
["no-worktree-no-merge-confirmed", "No worktree"],
|
|
["non-retryable-provider-error", "Provider error"],
|
|
] 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(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", "non-retryable-provider-error"] 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, status: undefined },
|
|
{
|
|
column: "in-review",
|
|
paused: true,
|
|
inReviewStall: { code: "merge-blocker", reason: "r", observedAt: "2026-05-13T00:00:00.000Z" },
|
|
status: undefined,
|
|
},
|
|
{
|
|
column: "in-progress",
|
|
paused: false,
|
|
inReviewStall: { code: "merge-blocker", reason: "r", observedAt: "2026-05-13T00:00:00.000Z" },
|
|
status: undefined,
|
|
},
|
|
] as const)("hides badge for non-canonical visibility cases", (task) => {
|
|
expect(shouldShowInReviewStallBadge(task)).toBe(false);
|
|
});
|
|
|
|
it.each([
|
|
["merge-blocker", "merging", false],
|
|
["merge-blocker", "merging-pr", false],
|
|
["merge-blocker", "merging-fix", false],
|
|
["merge-blocker", undefined, true],
|
|
["merge-retries-exhausted", "merging", true],
|
|
["transient-merge-status-no-owner", "merging", true],
|
|
["non-retryable-provider-error", undefined, true],
|
|
["non-retryable-provider-error", "merging", true],
|
|
["no-worktree-no-merge-confirmed", undefined, false],
|
|
["no-worktree-no-merge-confirmed", "merging", false],
|
|
] as const)("badge visibility for %s with status %s is %s", (code, status, expected) => {
|
|
expect(
|
|
shouldShowInReviewStallBadge({
|
|
column: "in-review",
|
|
paused: false,
|
|
status,
|
|
inReviewStall: { code, reason: "r", observedAt: "2026-05-13T00:00:00.000Z" },
|
|
}),
|
|
).toBe(expected);
|
|
});
|
|
|
|
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("In-review stall");
|
|
});
|
|
|
|
it("returns deadlock disposition copy when paused reason indicates auto-dispose", () => {
|
|
const copy = getInReviewStallDeadlockCopy({
|
|
pausedReason: "in-review-stall-deadlock",
|
|
log: [],
|
|
} as any);
|
|
|
|
expect(copy).toMatchObject({
|
|
headline: "In-review deadlock auto-disposed",
|
|
});
|
|
expect(copy?.nextAction).toContain("unpause");
|
|
});
|
|
});
|