fix(engine): resume execution after review revise

This commit is contained in:
Phil Larson
2026-05-29 10:59:25 -07:00
parent b265d6b395
commit 6b57e7cbf6
6 changed files with 91 additions and 20 deletions

View File

@@ -193,7 +193,7 @@ describe("Workflow Steps Execution", () => {
});
describe("FN-5436: pending-review skip on no-fn_task_done exit", () => {
it("parks in-review immediately when code review REVISE is pending", async () => {
it("does not park in-review when code review REVISE requires more executor work", async () => {
const store = createMockStore();
const baseTask = {
id: "FN-5436-A",
@@ -240,22 +240,20 @@ describe("Workflow Steps Execution", () => {
await executor.execute(baseTask as any);
expect(mockedCreateFnAgent).toHaveBeenCalledTimes(1);
expect(mockedCreateFnAgent).toHaveBeenCalledTimes(4);
expect(store.updateTask).toHaveBeenCalledWith("FN-5436-A", expect.objectContaining({
status: "failed",
error: "Agent finished without calling fn_task_done (after 3 retries)",
}));
expect(store.moveTask).toHaveBeenCalledWith("FN-5436-A", "todo", { preserveProgress: true });
expect(store.moveTask).not.toHaveBeenCalledWith("FN-5436-A", "in-review");
expect(store.updateTask).not.toHaveBeenCalledWith("FN-5436-A", {
status: "failed",
error: "executor-exit-while-review-pending",
});
expect(store.updateTask).not.toHaveBeenCalledWith("FN-5436-A", expect.objectContaining({ taskDoneRetryCount: expect.anything() }));
expect(store.moveTask).toHaveBeenCalledWith("FN-5436-A", "in-review");
expect(store.logEntry).toHaveBeenCalledWith(
"FN-5436-A",
expect.stringContaining("blocked on pending review (code-review-revise-outstanding)"),
undefined,
expect.objectContaining({ agentId: "executor" }),
);
expect(onError).not.toHaveBeenCalledWith(
expect(onError).toHaveBeenCalledWith(
expect.objectContaining({ id: "FN-5436-A" }),
expect.objectContaining({ message: "executor-exit-while-review-pending" }),
expect.objectContaining({ message: "Agent finished without calling fn_task_done (after 3 retries)" }),
);
});

View File

@@ -710,6 +710,31 @@ describe("ProjectEngine merge error recovery", () => {
expect(store.moveTask).toHaveBeenCalledWith(TASK_ID, "done");
});
it("auto-finalizes merge-confirmed tasks with stale transient merging status", async () => {
const store = makeStore({
tasks: [
makeTask({
mergeDetails: { mergeConfirmed: true },
status: "merging",
error: "stale transient merge state",
}),
],
});
const engine = createEngine(store);
await runMergeCycle(engine);
expect(store.updateTask).toHaveBeenCalledWith(TASK_ID, { paused: false, status: null, error: null });
expect(store.moveTask).toHaveBeenCalledWith(TASK_ID, "done");
expect(store.updateTask).not.toHaveBeenCalledWith(
TASK_ID,
expect.objectContaining({
status: "failed",
error: expect.stringContaining("finalization blocked"),
}),
);
});
it("does not park merge-confirmed tasks as failed when finalize loses in-review ownership", async () => {
const store = makeStore({
tasks: [

View File

@@ -4002,6 +4002,42 @@ describe("SelfHealingManager", () => {
managerWithRecovery.stop();
});
it("auto-finalizes merge-confirmed tasks with stale transient merging status", async () => {
const managerWithRecovery = new SelfHealingManager(store, {
rootDir: "/tmp/test-project",
});
(store.listTasks as ReturnType<typeof vi.fn>).mockResolvedValue([
{
id: "FN-354",
column: "in-review",
paused: false,
status: "merging",
error: "stale transient merge state",
mergeDetails: {
mergeConfirmed: true,
mergedAt: "2026-01-01T00:00:00.000Z",
},
steps: [{ status: "done" }],
workflowStepResults: [],
log: [],
},
]);
const result = await managerWithRecovery.recoverMergedReviewTasks();
expect(result).toBe(1);
expect(store.updateTask).toHaveBeenCalledWith("FN-354", {
paused: false,
status: null,
error: null,
mergeRetries: 0,
});
expect(store.moveTask).toHaveBeenCalledWith("FN-354", "done");
managerWithRecovery.stop();
});
});
describe("recoverStuckMergeDeadlocks", () => {