test(FN-4086): add stranded-todo recovery regression coverage
Fusion-Task-Id: FN-4086 Fusion-Task-Lineage: b2338fbc-960c-4540-9c19-216462525f0c
This commit is contained in:
@@ -1578,3 +1578,33 @@ describe("TaskExecutor fn_task_done blockers", () => {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe("TaskExecutor recoverCompletedTask", () => {
|
||||
beforeEach(() => {
|
||||
resetExecutorMocks();
|
||||
});
|
||||
|
||||
it("uses todo -> in-progress -> in-review transitions for todo-origin recovery", async () => {
|
||||
const store = createMockStore();
|
||||
const executor = new TaskExecutor(store as any, "/tmp/test");
|
||||
|
||||
const task = {
|
||||
id: "FN-4086",
|
||||
title: "Recover todo completed task",
|
||||
description: "Recover todo completed task",
|
||||
column: "todo",
|
||||
dependencies: [],
|
||||
steps: [{ name: "s1", status: "done" }],
|
||||
currentStep: 1,
|
||||
log: [],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
} as any;
|
||||
|
||||
const ok = await executor.recoverCompletedTask(task);
|
||||
|
||||
expect(ok).toBe(true);
|
||||
expect(store.moveTask).toHaveBeenNthCalledWith(1, "FN-4086", "in-progress");
|
||||
expect(store.moveTask).toHaveBeenNthCalledWith(2, "FN-4086", "in-review");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1659,6 +1659,111 @@ describe("SelfHealingManager", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("recoverStrandedCompletedTodoTasks", () => {
|
||||
it("promotes completed todo tasks and calls recover fn once per qualifying task", async () => {
|
||||
const recoverFn = vi.fn().mockResolvedValue(true);
|
||||
const getExecuting = vi.fn().mockReturnValue(new Set<string>());
|
||||
|
||||
const managerWithRecovery = new SelfHealingManager(store, {
|
||||
rootDir: "/tmp/test-project",
|
||||
recoverCompletedTask: recoverFn,
|
||||
getExecutingTaskIds: getExecuting,
|
||||
});
|
||||
|
||||
(store.listTasks as ReturnType<typeof vi.fn>).mockResolvedValue([
|
||||
{
|
||||
id: "FN-101",
|
||||
column: "todo",
|
||||
paused: false,
|
||||
error: null,
|
||||
reviewLevel: 2,
|
||||
steps: [{ status: "done" }, { status: "skipped" }],
|
||||
},
|
||||
]);
|
||||
|
||||
const result = await managerWithRecovery.recoverStrandedCompletedTodoTasks();
|
||||
|
||||
expect(result).toBe(1);
|
||||
expect(store.listTasks).toHaveBeenCalledWith({ column: "todo", slim: true });
|
||||
expect(recoverFn).toHaveBeenCalledTimes(1);
|
||||
expect(recoverFn).toHaveBeenCalledWith(expect.objectContaining({ id: "FN-101" }));
|
||||
|
||||
managerWithRecovery.stop();
|
||||
});
|
||||
|
||||
it("leaves incomplete/error/executing todo tasks untouched", async () => {
|
||||
const recoverFn = vi.fn().mockResolvedValue(true);
|
||||
const getExecuting = vi.fn().mockReturnValue(new Set<string>(["FN-105"]));
|
||||
|
||||
const managerWithRecovery = new SelfHealingManager(store, {
|
||||
rootDir: "/tmp/test-project",
|
||||
recoverCompletedTask: recoverFn,
|
||||
getExecutingTaskIds: getExecuting,
|
||||
});
|
||||
|
||||
(store.listTasks as ReturnType<typeof vi.fn>).mockResolvedValue([
|
||||
{
|
||||
id: "FN-103",
|
||||
column: "todo",
|
||||
paused: false,
|
||||
error: null,
|
||||
steps: [{ status: "done" }, { status: "pending" }],
|
||||
},
|
||||
{
|
||||
id: "FN-104",
|
||||
column: "todo",
|
||||
paused: false,
|
||||
error: "failed earlier",
|
||||
steps: [{ status: "done" }],
|
||||
},
|
||||
{
|
||||
id: "FN-105",
|
||||
column: "todo",
|
||||
paused: false,
|
||||
error: null,
|
||||
steps: [{ status: "done" }],
|
||||
},
|
||||
]);
|
||||
|
||||
const result = await managerWithRecovery.recoverStrandedCompletedTodoTasks();
|
||||
|
||||
expect(result).toBe(0);
|
||||
expect(recoverFn).not.toHaveBeenCalled();
|
||||
|
||||
managerWithRecovery.stop();
|
||||
});
|
||||
|
||||
it("recovers blockedBy todo tasks when all steps are complete", async () => {
|
||||
const recoverFn = vi.fn().mockResolvedValue(true);
|
||||
|
||||
const managerWithRecovery = new SelfHealingManager(store, {
|
||||
rootDir: "/tmp/test-project",
|
||||
recoverCompletedTask: recoverFn,
|
||||
getExecutingTaskIds: () => new Set<string>(),
|
||||
});
|
||||
|
||||
(store.listTasks as ReturnType<typeof vi.fn>).mockResolvedValue([
|
||||
{
|
||||
id: "FN-106",
|
||||
column: "todo",
|
||||
paused: false,
|
||||
blockedBy: "FN-001",
|
||||
status: "queued",
|
||||
error: null,
|
||||
reviewLevel: 0,
|
||||
steps: [{ status: "done" }, { status: "done" }],
|
||||
},
|
||||
]);
|
||||
|
||||
const result = await managerWithRecovery.recoverStrandedCompletedTodoTasks();
|
||||
|
||||
expect(result).toBe(1);
|
||||
expect(recoverFn).toHaveBeenCalledWith(expect.objectContaining({ id: "FN-106" }));
|
||||
|
||||
managerWithRecovery.stop();
|
||||
});
|
||||
});
|
||||
|
||||
describe("recoverMissingWorktreeReviewFailures", () => {
|
||||
it("requeues failed in-review tasks with missing-worktree session-start errors", async () => {
|
||||
const managerWithRecovery = new SelfHealingManager(store, {
|
||||
@@ -5606,6 +5711,7 @@ describe("maintenance cycle concurrency", () => {
|
||||
}) as any);
|
||||
|
||||
makeSlow("recoverCompletedTasks");
|
||||
makeSlow("recoverStrandedCompletedTodoTasks");
|
||||
makeSlow("recoverStaleIncompleteReviewTasks");
|
||||
makeSlow("recoverInterruptedMergingTasks");
|
||||
makeSlow("recoverStaleMergingStatus");
|
||||
@@ -5635,6 +5741,7 @@ describe("maintenance cycle concurrency", () => {
|
||||
it("one failing batch 2 operation does not abort the batch", async () => {
|
||||
const batch2Operations = [
|
||||
"recoverCompletedTasks",
|
||||
"recoverStrandedCompletedTodoTasks",
|
||||
"recoverStaleIncompleteReviewTasks",
|
||||
"recoverInterruptedMergingTasks",
|
||||
"recoverStaleMergingStatus",
|
||||
|
||||
@@ -158,7 +158,7 @@ const STRANDED_COMPLETED_TODO_ACTIVE_STATUSES = new Set([
|
||||
"in-progress",
|
||||
"planning",
|
||||
"specifying",
|
||||
"queued",
|
||||
|
||||
"merging",
|
||||
"merging-pr",
|
||||
"merging-fix",
|
||||
|
||||
Reference in New Issue
Block a user