kin/tests/test_api.py
Gros Frumos bf38532f59 Add cancelled status for tasks
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 18:22:17 +02:00

276 lines
8.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Tests for web/api.py — new task endpoints (pipeline, approve, reject, full)."""
import pytest
from pathlib import Path
from fastapi.testclient import TestClient
# Patch DB_PATH before importing app
import web.api as api_module
@pytest.fixture
def client(tmp_path):
db_path = tmp_path / "test.db"
api_module.DB_PATH = db_path
from web.api import app
c = TestClient(app)
# Seed data
c.post("/api/projects", json={"id": "p1", "name": "P1", "path": "/p1"})
c.post("/api/tasks", json={"project_id": "p1", "title": "Fix bug"})
return c
def test_get_task(client):
r = client.get("/api/tasks/P1-001")
assert r.status_code == 200
assert r.json()["title"] == "Fix bug"
def test_get_task_not_found(client):
r = client.get("/api/tasks/NOPE")
assert r.status_code == 404
def test_task_pipeline_empty(client):
r = client.get("/api/tasks/P1-001/pipeline")
assert r.status_code == 200
assert r.json() == []
def test_task_pipeline_with_logs(client):
# Insert agent logs directly
from core.db import init_db
from core import models
conn = init_db(api_module.DB_PATH)
models.log_agent_run(conn, "p1", "debugger", "execute",
task_id="P1-001", output_summary="Found bug",
tokens_used=1000, duration_seconds=5, success=True)
models.log_agent_run(conn, "p1", "tester", "execute",
task_id="P1-001", output_summary="Tests pass",
tokens_used=500, duration_seconds=3, success=True)
conn.close()
r = client.get("/api/tasks/P1-001/pipeline")
assert r.status_code == 200
steps = r.json()
assert len(steps) == 2
assert steps[0]["agent_role"] == "debugger"
assert steps[0]["output_summary"] == "Found bug"
assert steps[1]["agent_role"] == "tester"
def test_task_full(client):
r = client.get("/api/tasks/P1-001/full")
assert r.status_code == 200
data = r.json()
assert data["id"] == "P1-001"
assert "pipeline_steps" in data
assert "related_decisions" in data
def test_task_full_not_found(client):
r = client.get("/api/tasks/NOPE/full")
assert r.status_code == 404
def test_approve_task(client):
# First set task to review
from core.db import init_db
from core import models
conn = init_db(api_module.DB_PATH)
models.update_task(conn, "P1-001", status="review")
conn.close()
r = client.post("/api/tasks/P1-001/approve", json={})
assert r.status_code == 200
assert r.json()["status"] == "done"
# Verify task is done
r = client.get("/api/tasks/P1-001")
assert r.json()["status"] == "done"
def test_approve_with_decision(client):
r = client.post("/api/tasks/P1-001/approve", json={
"decision_title": "Use AbortController",
"decision_description": "Fix race condition with AbortController",
"decision_type": "decision",
})
assert r.status_code == 200
assert r.json()["decision"] is not None
assert r.json()["decision"]["title"] == "Use AbortController"
def test_approve_not_found(client):
r = client.post("/api/tasks/NOPE/approve", json={})
assert r.status_code == 404
def test_reject_task(client):
from core.db import init_db
from core import models
conn = init_db(api_module.DB_PATH)
models.update_task(conn, "P1-001", status="review")
conn.close()
r = client.post("/api/tasks/P1-001/reject", json={
"reason": "Didn't fix the root cause"
})
assert r.status_code == 200
assert r.json()["status"] == "pending"
# Verify task is pending with review reason
r = client.get("/api/tasks/P1-001")
data = r.json()
assert data["status"] == "pending"
assert data["review"]["rejected"] == "Didn't fix the root cause"
def test_reject_not_found(client):
r = client.post("/api/tasks/NOPE/reject", json={"reason": "bad"})
assert r.status_code == 404
def test_task_pipeline_not_found(client):
r = client.get("/api/tasks/NOPE/pipeline")
assert r.status_code == 404
def test_running_endpoint_no_pipeline(client):
r = client.get("/api/tasks/P1-001/running")
assert r.status_code == 200
assert r.json()["running"] is False
def test_running_endpoint_with_pipeline(client):
from core.db import init_db
from core import models
conn = init_db(api_module.DB_PATH)
models.create_pipeline(conn, "P1-001", "p1", "debug",
[{"role": "debugger"}])
conn.close()
r = client.get("/api/tasks/P1-001/running")
assert r.status_code == 200
assert r.json()["running"] is True
def test_running_endpoint_not_found(client):
r = client.get("/api/tasks/NOPE/running")
assert r.status_code == 404
def test_run_sets_in_progress(client):
"""POST /run should set task to in_progress immediately."""
r = client.post("/api/tasks/P1-001/run")
assert r.status_code == 202
r = client.get("/api/tasks/P1-001")
assert r.json()["status"] == "in_progress"
def test_run_not_found(client):
r = client.post("/api/tasks/NOPE/run")
assert r.status_code == 404
def test_run_with_allow_write(client):
"""POST /run with allow_write=true should be accepted."""
r = client.post("/api/tasks/P1-001/run", json={"allow_write": True})
assert r.status_code == 202
def test_run_with_empty_body(client):
"""POST /run with empty JSON body should default allow_write=false."""
r = client.post("/api/tasks/P1-001/run", json={})
assert r.status_code == 202
def test_run_without_body(client):
"""POST /run without body should be backwards-compatible."""
r = client.post("/api/tasks/P1-001/run")
assert r.status_code == 202
def test_project_summary_includes_review(client):
from core.db import init_db
from core import models
conn = init_db(api_module.DB_PATH)
models.update_task(conn, "P1-001", status="review")
conn.close()
r = client.get("/api/projects")
projects = r.json()
assert projects[0]["review_tasks"] == 1
def test_audit_not_found(client):
r = client.post("/api/projects/NOPE/audit")
assert r.status_code == 404
def test_audit_apply(client):
"""POST /audit/apply should mark tasks as done."""
r = client.post("/api/projects/p1/audit/apply",
json={"task_ids": ["P1-001"]})
assert r.status_code == 200
assert r.json()["count"] == 1
assert "P1-001" in r.json()["updated"]
# Verify task is done
r = client.get("/api/tasks/P1-001")
assert r.json()["status"] == "done"
def test_audit_apply_not_found(client):
r = client.post("/api/projects/NOPE/audit/apply",
json={"task_ids": ["P1-001"]})
assert r.status_code == 404
def test_audit_apply_wrong_project(client):
"""Tasks not belonging to the project should be skipped."""
r = client.post("/api/projects/p1/audit/apply",
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", "cancelled"])
def test_patch_task_all_valid_statuses(client, status):
"""Все 6 допустимых статусов должны приниматься."""
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