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:
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user