feat(FN-3939): audit and patch PR #59 residual defects, add retry parity an

Completes FN-3939 audit work for PR #59 residual defects by refactoring the mission execution loop in the engine (halving its complexity), adding self-healing test coverage, and wiring in task workflow route improvements. Retry and validation behavior updates are documented in the changeset and skil

Fusion-Task-Id: FN-3939
This commit is contained in:
Fusion
2026-05-10 16:19:04 -07:00
committed by gsxdsm
parent 60d16aa267
commit 76c00fbadd
15 changed files with 308 additions and 94 deletions

View File

@@ -429,7 +429,7 @@ describe("POST /tasks/:id/retry", () => {
mergeRetries: 0,
});
expect(store.moveTask).not.toHaveBeenCalled();
expect(store.logEntry).toHaveBeenCalledWith("KB-001", "Retry requested from dashboard (in-review retry, mergeRetries reset)");
expect(store.logEntry).toHaveBeenCalledWith("KB-001", "Retry requested from dashboard (in-review merge retry, mergeRetries reset)");
});
it("retries a stuck-killed in-review task without moving columns", async () => {
@@ -451,7 +451,7 @@ describe("POST /tasks/:id/retry", () => {
mergeRetries: 0,
});
expect(store.moveTask).not.toHaveBeenCalled();
expect(store.logEntry).toHaveBeenCalledWith("KB-001", "Retry requested from dashboard (in-review retry, mergeRetries reset)");
expect(store.logEntry).toHaveBeenCalledWith("KB-001", "Retry requested from dashboard (in-review merge retry, mergeRetries reset)");
});
it("preserves worktree/branch when retrying in-review task", async () => {
@@ -482,6 +482,102 @@ describe("POST /tasks/:id/retry", () => {
expect(updateCall).not.toHaveProperty("nextRecoveryAt");
});
it("retries execution-failed in-review task by moving to todo with progress preserved", async () => {
const executionFailedTask = {
...FAKE_TASK_DETAIL,
column: "in-review" as const,
status: "failed",
steps: [
{ name: "Step 0", status: "done" },
{ name: "Step 1", status: "in-progress" },
{ name: "Step 2", status: "pending" },
],
};
const movedTask = { ...executionFailedTask, column: "todo" as const, status: undefined };
(store.getTask as ReturnType<typeof vi.fn>).mockResolvedValueOnce(executionFailedTask);
(store.updateTask as ReturnType<typeof vi.fn>).mockResolvedValue(executionFailedTask);
(store.moveTask as ReturnType<typeof vi.fn>).mockResolvedValue(movedTask);
const res = await REQUEST(buildApp(), "POST", "/api/tasks/KB-001/retry", JSON.stringify({}), {
"Content-Type": "application/json",
});
expect(res.status).toBe(200);
expect(store.updateTask).toHaveBeenCalledWith("KB-001", {
status: null,
error: null,
stuckKillCount: 0,
});
expect(store.moveTask).toHaveBeenCalledWith("KB-001", "todo", { preserveProgress: true });
expect(store.logEntry).toHaveBeenCalledWith(
"KB-001",
"Retry requested from dashboard (execution failure in-review → todo, preserving progress)",
);
});
it("retries merge-failed in-review task by staying in-review with mergeRetries reset", async () => {
const mergeFailedTask = {
...FAKE_TASK_DETAIL,
column: "in-review" as const,
status: "failed",
steps: [
{ name: "Step 0", status: "done" },
{ name: "Step 1", status: "done" },
{ name: "Step 2", status: "done" },
],
};
(store.getTask as ReturnType<typeof vi.fn>)
.mockResolvedValueOnce(mergeFailedTask)
.mockResolvedValueOnce(mergeFailedTask);
(store.updateTask as ReturnType<typeof vi.fn>).mockResolvedValue(mergeFailedTask);
const res = await REQUEST(buildApp(), "POST", "/api/tasks/KB-001/retry", JSON.stringify({}), {
"Content-Type": "application/json",
});
expect(res.status).toBe(200);
expect(store.updateTask).toHaveBeenCalledWith("KB-001", {
status: null,
error: null,
stuckKillCount: 0,
mergeRetries: 0,
});
expect(store.moveTask).not.toHaveBeenCalled();
expect(store.logEntry).toHaveBeenCalledWith(
"KB-001",
"Retry requested from dashboard (in-review merge retry, mergeRetries reset)",
);
});
it("retries stuck-killed in-review task with incomplete steps moves to todo", async () => {
const stuckTask = {
...FAKE_TASK_DETAIL,
column: "in-review" as const,
status: "stuck-killed",
steps: [
{ name: "Step 0", status: "done" },
{ name: "Step 1", status: "pending" },
],
};
const movedTask = { ...stuckTask, column: "todo" as const, status: undefined };
(store.getTask as ReturnType<typeof vi.fn>).mockResolvedValueOnce(stuckTask);
(store.updateTask as ReturnType<typeof vi.fn>).mockResolvedValue(stuckTask);
(store.moveTask as ReturnType<typeof vi.fn>).mockResolvedValue(movedTask);
const res = await REQUEST(buildApp(), "POST", "/api/tasks/KB-001/retry", JSON.stringify({}), {
"Content-Type": "application/json",
});
expect(res.status).toBe(200);
expect(store.moveTask).toHaveBeenCalledWith("KB-001", "todo", { preserveProgress: true });
expect(store.logEntry).toHaveBeenCalledWith(
"KB-001",
"Retry requested from dashboard (execution failure in-review → todo, preserving progress)",
);
const updateCall = (store.updateTask as ReturnType<typeof vi.fn>).mock.calls[0][1];
expect(updateCall).not.toHaveProperty("mergeRetries");
});
it("retries a stranded planning triage task in triage and removes stale prompt", async () => {
const tempRoot = mkdtempSync(join(tmpdir(), "kb-task-retry-spec-"));
const taskDir = join(tempRoot, ".fusion", "tasks", "FN-001");