test(FN-4885): complete Step 4 — expand self-healing stall disposition coverage
Fusion-Task-Id: FN-4885 Fusion-Task-Lineage: 31110eed-5ca3-4d67-8f11-581185056dc6
This commit is contained in:
committed by
gsxdsm
parent
7555f3d12f
commit
4e9bfd6062
@@ -158,6 +158,7 @@ function createMockStore(overrides: Record<string, unknown> = {}): TaskStore & E
|
||||
walCheckpoint: vi.fn().mockReturnValue({ busy: 0, log: 5, checkpointed: 5 }),
|
||||
listTasks: vi.fn().mockResolvedValue([]),
|
||||
createTask: vi.fn().mockResolvedValue({ id: "FN-RESCUE", lineageId: "lin-rescue" }),
|
||||
recordRunAuditEvent: vi.fn().mockResolvedValue(undefined),
|
||||
getRootDir: vi.fn().mockReturnValue("/tmp/test-project"),
|
||||
clearStaleExecutionStartBranchReferences: vi.fn().mockReturnValue([]),
|
||||
...overrides,
|
||||
@@ -4707,6 +4708,141 @@ describe("SelfHealingManager", () => {
|
||||
expect(store.logEntry).not.toHaveBeenCalled();
|
||||
managerWithRecovery.stop();
|
||||
});
|
||||
|
||||
it("auto-disposes after three identical merge-blocker stalls", async () => {
|
||||
vi.setSystemTime(new Date("2026-01-01T00:10:00.000Z"));
|
||||
const managerWithRecovery = new SelfHealingManager(store, { rootDir: "/tmp/test-project" });
|
||||
const reason = "task is marked 'failed': Failed to create worktree after 3 attempts: Branch fusion/fn-9999 conflict could not be auto-resolved";
|
||||
(store.getSettings as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
taskStuckTimeoutMs: 60_000,
|
||||
inReviewStallDeadlockThreshold: 3,
|
||||
});
|
||||
(store.listTasks as ReturnType<typeof vi.fn>).mockResolvedValue([
|
||||
staleMergingTask({
|
||||
id: "FN-9999",
|
||||
status: "failed",
|
||||
error: "Failed to create worktree after 3 attempts: Branch fusion/fn-9999 conflict could not be auto-resolved",
|
||||
branch: "fusion/fn-9999",
|
||||
worktree: "/tmp/FN-9999",
|
||||
log: [
|
||||
{ timestamp: "2026-01-01T00:01:00.000Z", action: `In-review stall surfaced [merge-blocker]: ${reason}` },
|
||||
{ timestamp: "2026-01-01T00:03:00.000Z", action: `In-review stall surfaced [merge-blocker]: ${reason}` },
|
||||
],
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(await managerWithRecovery.surfaceInReviewStalls()).toBe(1);
|
||||
expect(store.updateTask).toHaveBeenCalledWith("FN-9999", expect.objectContaining({
|
||||
paused: true,
|
||||
pausedReason: "in-review-stall-deadlock",
|
||||
status: "failed",
|
||||
}));
|
||||
expect(store.logEntry).toHaveBeenCalledWith(
|
||||
"FN-9999",
|
||||
expect.stringContaining("In-review stall auto-disposed [merge-blocker]: deadlock-prevention threshold reached after 3 identical stalls"),
|
||||
);
|
||||
expect(store.logEntry).not.toHaveBeenCalledWith(
|
||||
"FN-9999",
|
||||
expect.stringContaining("In-review stall surfaced [merge-blocker]"),
|
||||
);
|
||||
expect(store.recordRunAuditEvent).toHaveBeenCalledWith(expect.objectContaining({
|
||||
domain: "database",
|
||||
mutationType: "task:in-review-stall-deadlock-disposed",
|
||||
target: "FN-9999",
|
||||
metadata: expect.objectContaining({
|
||||
code: "merge-blocker",
|
||||
reason,
|
||||
repetitionCount: 3,
|
||||
threshold: 3,
|
||||
}),
|
||||
}));
|
||||
managerWithRecovery.stop();
|
||||
});
|
||||
|
||||
it("does not dispose below threshold", async () => {
|
||||
vi.setSystemTime(new Date("2026-01-01T00:10:00.000Z"));
|
||||
const managerWithRecovery = new SelfHealingManager(store, { rootDir: "/tmp/test-project" });
|
||||
const reason = "task is marked 'failed': Failed to create worktree after 3 attempts: Branch fusion/fn-9999 conflict could not be auto-resolved";
|
||||
(store.getSettings as ReturnType<typeof vi.fn>).mockResolvedValue({ taskStuckTimeoutMs: 60_000, inReviewStallDeadlockThreshold: 3 });
|
||||
(store.listTasks as ReturnType<typeof vi.fn>).mockResolvedValue([
|
||||
staleMergingTask({
|
||||
id: "FN-9999",
|
||||
status: "failed",
|
||||
error: "Failed to create worktree after 3 attempts: Branch fusion/fn-9999 conflict could not be auto-resolved",
|
||||
worktree: "/tmp/FN-9999",
|
||||
log: [{ timestamp: "2026-01-01T00:01:00.000Z", action: `In-review stall surfaced [merge-blocker]: ${reason}` }],
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(await managerWithRecovery.surfaceInReviewStalls()).toBe(1);
|
||||
expect(store.logEntry).toHaveBeenCalledWith("FN-9999", expect.stringContaining("In-review stall surfaced [merge-blocker]:"));
|
||||
expect(store.updateTask).not.toHaveBeenCalled();
|
||||
expect(store.recordRunAuditEvent).not.toHaveBeenCalled();
|
||||
managerWithRecovery.stop();
|
||||
});
|
||||
|
||||
it("does not dispose when threshold is disabled", async () => {
|
||||
vi.setSystemTime(new Date("2026-01-01T00:10:00.000Z"));
|
||||
const managerWithRecovery = new SelfHealingManager(store, { rootDir: "/tmp/test-project" });
|
||||
const reason = "task is marked 'failed': Failed to create worktree after 3 attempts: Branch fusion/fn-9999 conflict could not be auto-resolved";
|
||||
(store.getSettings as ReturnType<typeof vi.fn>).mockResolvedValue({ taskStuckTimeoutMs: 60_000, inReviewStallDeadlockThreshold: 0 });
|
||||
(store.listTasks as ReturnType<typeof vi.fn>).mockResolvedValue([
|
||||
staleMergingTask({
|
||||
id: "FN-9999",
|
||||
status: "failed",
|
||||
error: "Failed to create worktree after 3 attempts: Branch fusion/fn-9999 conflict could not be auto-resolved",
|
||||
worktree: "/tmp/FN-9999",
|
||||
log: [
|
||||
{ timestamp: "2026-01-01T00:01:00.000Z", action: `In-review stall surfaced [merge-blocker]: ${reason}` },
|
||||
{ timestamp: "2026-01-01T00:02:00.000Z", action: `In-review stall surfaced [merge-blocker]: ${reason}` },
|
||||
{ timestamp: "2026-01-01T00:03:00.000Z", action: `In-review stall surfaced [merge-blocker]: ${reason}` },
|
||||
],
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(await managerWithRecovery.surfaceInReviewStalls()).toBe(1);
|
||||
expect(store.logEntry).toHaveBeenCalledWith("FN-9999", expect.stringContaining("In-review stall surfaced [merge-blocker]:"));
|
||||
expect(store.updateTask).not.toHaveBeenCalled();
|
||||
expect(store.recordRunAuditEvent).not.toHaveBeenCalled();
|
||||
managerWithRecovery.stop();
|
||||
});
|
||||
|
||||
it("does not accumulate when reasons differ and no-ops when already paused", async () => {
|
||||
vi.setSystemTime(new Date("2026-01-01T00:10:00.000Z"));
|
||||
const managerWithRecovery = new SelfHealingManager(store, { rootDir: "/tmp/test-project" });
|
||||
const baseError = "Failed to create worktree after 3 attempts: Branch fusion/fn-9999 conflict could not be auto-resolved";
|
||||
const currentReason = `task is marked 'failed': ${baseError}`;
|
||||
(store.getSettings as ReturnType<typeof vi.fn>).mockResolvedValue({ taskStuckTimeoutMs: 60_000, inReviewStallDeadlockThreshold: 3 });
|
||||
(store.listTasks as ReturnType<typeof vi.fn>)
|
||||
.mockResolvedValueOnce([
|
||||
staleMergingTask({
|
||||
id: "FN-9999",
|
||||
status: "failed",
|
||||
error: baseError,
|
||||
worktree: "/tmp/FN-9999",
|
||||
log: [
|
||||
{ timestamp: "2026-01-01T00:01:00.000Z", action: "In-review stall surfaced [merge-blocker]: task is marked 'failed': other reason 1" },
|
||||
{ timestamp: "2026-01-01T00:02:00.000Z", action: "In-review stall surfaced [merge-blocker]: task is marked 'failed': other reason 2" },
|
||||
{ timestamp: "2026-01-01T00:03:00.000Z", action: `In-review stall surfaced [merge-blocker]: ${currentReason}` },
|
||||
],
|
||||
}),
|
||||
])
|
||||
.mockResolvedValueOnce([
|
||||
staleMergingTask({ id: "FN-9999", paused: true, status: "failed", error: baseError, worktree: "/tmp/FN-9999" }),
|
||||
]);
|
||||
|
||||
expect(await managerWithRecovery.surfaceInReviewStalls()).toBe(1);
|
||||
expect(store.logEntry).toHaveBeenCalledWith("FN-9999", expect.stringContaining("In-review stall surfaced [merge-blocker]:"));
|
||||
expect(store.updateTask).not.toHaveBeenCalled();
|
||||
expect(store.recordRunAuditEvent).not.toHaveBeenCalled();
|
||||
|
||||
vi.clearAllMocks();
|
||||
expect(await managerWithRecovery.surfaceInReviewStalls()).toBe(0);
|
||||
expect(store.logEntry).not.toHaveBeenCalled();
|
||||
expect(store.updateTask).not.toHaveBeenCalled();
|
||||
expect(store.recordRunAuditEvent).not.toHaveBeenCalled();
|
||||
managerWithRecovery.stop();
|
||||
});
|
||||
});
|
||||
|
||||
describe("surfaceStalePausedReviews", () => {
|
||||
|
||||
Reference in New Issue
Block a user