feat(HAI-055): add failed task status handling with retry support
- Set task status to 'failed' on execution failure in engine executor - Add failed indicator styling on TaskCard component - Add POST /tasks/:id/retry API endpoint and client function - Add retry button in TaskDetailModal for failed tasks - Add tests for failed indicator, retry endpoint, and modal behavior
This commit is contained in:
@@ -16,6 +16,7 @@ function createMockStore(overrides: Partial<TaskStore> = {}): TaskStore {
|
||||
mergeTask: vi.fn(),
|
||||
getSettings: vi.fn().mockResolvedValue({}),
|
||||
updateSettings: vi.fn(),
|
||||
logEntry: vi.fn().mockResolvedValue(undefined),
|
||||
...overrides,
|
||||
} as unknown as TaskStore;
|
||||
}
|
||||
@@ -143,6 +144,61 @@ describe("GET /tasks/:id", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("POST /tasks/:id/retry", () => {
|
||||
let store: TaskStore;
|
||||
|
||||
beforeEach(() => {
|
||||
store = createMockStore();
|
||||
});
|
||||
|
||||
function buildApp() {
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.use("/api", createApiRoutes(store));
|
||||
return app;
|
||||
}
|
||||
|
||||
it("retries a failed task and moves it to todo", async () => {
|
||||
const failedTask = { ...FAKE_TASK_DETAIL, status: "failed" };
|
||||
const movedTask = { ...FAKE_TASK_DETAIL, column: "todo", status: undefined };
|
||||
(store.getTask as ReturnType<typeof vi.fn>).mockResolvedValue(failedTask);
|
||||
(store.updateTask as ReturnType<typeof vi.fn>).mockResolvedValue(failedTask);
|
||||
(store.moveTask as ReturnType<typeof vi.fn>).mockResolvedValue(movedTask);
|
||||
|
||||
const res = await REQUEST(buildApp(), "POST", "/api/tasks/HAI-001/retry", JSON.stringify({}), {
|
||||
"Content-Type": "application/json",
|
||||
});
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(store.updateTask).toHaveBeenCalledWith("HAI-001", { status: undefined });
|
||||
expect(store.moveTask).toHaveBeenCalledWith("HAI-001", "todo");
|
||||
});
|
||||
|
||||
it("returns 400 when task is not in failed state", async () => {
|
||||
const activeTask = { ...FAKE_TASK_DETAIL, status: "executing" };
|
||||
(store.getTask as ReturnType<typeof vi.fn>).mockResolvedValue(activeTask);
|
||||
|
||||
const res = await REQUEST(buildApp(), "POST", "/api/tasks/HAI-001/retry", JSON.stringify({}), {
|
||||
"Content-Type": "application/json",
|
||||
});
|
||||
|
||||
expect(res.status).toBe(400);
|
||||
expect(res.body.error).toContain("not in a failed state");
|
||||
});
|
||||
|
||||
it("returns 400 when task is not in in-progress column", async () => {
|
||||
const doneTask = { ...FAKE_TASK_DETAIL, column: "done", status: "failed" };
|
||||
(store.getTask as ReturnType<typeof vi.fn>).mockResolvedValue(doneTask);
|
||||
|
||||
const res = await REQUEST(buildApp(), "POST", "/api/tasks/HAI-001/retry", JSON.stringify({}), {
|
||||
"Content-Type": "application/json",
|
||||
});
|
||||
|
||||
expect(res.status).toBe(400);
|
||||
expect(res.body.error).toContain("not in a failed state");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Attachment routes", () => {
|
||||
const FAKE_ATTACHMENT: TaskAttachment = {
|
||||
filename: "1234-screenshot.png",
|
||||
|
||||
@@ -108,6 +108,23 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
||||
}
|
||||
});
|
||||
|
||||
// Retry failed task
|
||||
router.post("/tasks/:id/retry", async (req, res) => {
|
||||
try {
|
||||
const task = await store.getTask(req.params.id);
|
||||
if (task.column !== "in-progress" || task.status !== "failed") {
|
||||
res.status(400).json({ error: "Task is not in a failed state" });
|
||||
return;
|
||||
}
|
||||
await store.updateTask(req.params.id, { status: undefined });
|
||||
await store.logEntry(req.params.id, "Retry requested from dashboard");
|
||||
const updated = await store.moveTask(req.params.id, "todo");
|
||||
res.json(updated);
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Upload attachment
|
||||
router.post("/tasks/:id/attachments", upload.single("file"), async (req, res) => {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user