diff --git a/tests/test_api.py b/tests/test_api.py index 028c466..48ce340 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -233,3 +233,44 @@ def test_audit_apply_wrong_project(client): json={"task_ids": ["WRONG-001"]}) assert r.status_code == 200 assert r.json()["count"] == 0 + + +# --------------------------------------------------------------------------- +# PATCH /api/tasks/{task_id} — смена статуса +# --------------------------------------------------------------------------- + +def test_patch_task_status(client): + """PATCH должен обновить статус и вернуть задачу.""" + r = client.patch("/api/tasks/P1-001", json={"status": "review"}) + assert r.status_code == 200 + data = r.json() + assert data["status"] == "review" + assert data["id"] == "P1-001" + + +def test_patch_task_status_persisted(client): + """После PATCH повторный GET должен возвращать новый статус.""" + client.patch("/api/tasks/P1-001", json={"status": "blocked"}) + r = client.get("/api/tasks/P1-001") + assert r.status_code == 200 + assert r.json()["status"] == "blocked" + + +@pytest.mark.parametrize("status", ["pending", "in_progress", "review", "done", "blocked"]) +def test_patch_task_all_valid_statuses(client, status): + """Все 5 допустимых статусов должны приниматься.""" + r = client.patch("/api/tasks/P1-001", json={"status": status}) + assert r.status_code == 200 + assert r.json()["status"] == status + + +def test_patch_task_invalid_status(client): + """Недопустимый статус → 400.""" + r = client.patch("/api/tasks/P1-001", json={"status": "flying"}) + assert r.status_code == 400 + + +def test_patch_task_not_found(client): + """Несуществующая задача → 404.""" + r = client.patch("/api/tasks/NOPE-999", json={"status": "done"}) + assert r.status_code == 404 diff --git a/web/api.py b/web/api.py index 45f616d..cd38861 100644 --- a/web/api.py +++ b/web/api.py @@ -137,6 +137,28 @@ def create_task(body: TaskCreate): return t +class TaskPatch(BaseModel): + status: str + + +VALID_STATUSES = {"pending", "in_progress", "review", "done", "blocked"} + + +@app.patch("/api/tasks/{task_id}") +def patch_task(task_id: str, body: TaskPatch): + if body.status not in VALID_STATUSES: + raise HTTPException(400, f"Invalid status '{body.status}'. Must be one of: {', '.join(VALID_STATUSES)}") + conn = get_conn() + t = models.get_task(conn, task_id) + if not t: + conn.close() + raise HTTPException(404, f"Task '{task_id}' not found") + models.update_task(conn, task_id, status=body.status) + t = models.get_task(conn, task_id) + conn.close() + return t + + @app.get("/api/tasks/{task_id}/pipeline") def get_task_pipeline(task_id: str): """Get agent_logs for a task (pipeline steps).""" diff --git a/web/frontend/src/api.ts b/web/frontend/src/api.ts index 3a4200c..4b44050 100644 --- a/web/frontend/src/api.ts +++ b/web/frontend/src/api.ts @@ -6,6 +6,16 @@ async function get(path: string): Promise { return res.json() } +async function patch(path: string, body: unknown): Promise { + const res = await fetch(`${BASE}${path}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }) + if (!res.ok) throw new Error(`${res.status} ${res.statusText}`) + return res.json() +} + async function post(path: string, body: unknown): Promise { const res = await fetch(`${BASE}${path}`, { method: 'POST', @@ -148,4 +158,6 @@ export const api = { post(`/projects/${projectId}/audit`, {}), auditApply: (projectId: string, taskIds: string[]) => post<{ updated: string[]; count: number }>(`/projects/${projectId}/audit/apply`, { task_ids: taskIds }), + patchTask: (id: string, data: { status: string }) => + patch(`/tasks/${id}`, data), } diff --git a/web/frontend/src/views/TaskDetail.vue b/web/frontend/src/views/TaskDetail.vue index b3a3659..8a0ad6b 100644 --- a/web/frontend/src/views/TaskDetail.vue +++ b/web/frontend/src/views/TaskDetail.vue @@ -185,6 +185,21 @@ async function runPipeline() { const hasSteps = computed(() => (task.value?.pipeline_steps?.length ?? 0) > 0) const isRunning = computed(() => task.value?.status === 'in_progress') + +const statusChanging = ref(false) + +async function changeStatus(newStatus: string) { + if (!task.value || newStatus === task.value.status) return + statusChanging.value = true + try { + const updated = await api.patchTask(props.id, { status: newStatus }) + task.value = { ...task.value, ...updated } + } catch (e: any) { + error.value = e.message + } finally { + statusChanging.value = false + } +}