276 lines
8.5 KiB
Python
276 lines
8.5 KiB
Python
"""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
|