Skip to content

Commit 40d7317

Browse files
committed
test(api): cover task admin endpoints
1 parent 3f0dec1 commit 40d7317

File tree

2 files changed

+121
-20
lines changed

2 files changed

+121
-20
lines changed

app.py

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -432,26 +432,6 @@ async def metrics() -> PlainTextResponse:
432432
return PlainTextResponse(content, media_type="text/plain; version=0.0.4")
433433

434434

435-
@app.get("/tasks/{task_id}")
436-
async def get_task(task_id: str, _admin_key: AdminKeyDep = None) -> dict[str, Any]:
437-
"""Get task status by ID."""
438-
if not _task_queue:
439-
raise HTTPException(status_code=503, detail="Task queue not initialized")
440-
task = await _task_queue.get_task(task_id)
441-
if not task:
442-
raise HTTPException(status_code=404, detail="Task not found")
443-
return {
444-
"task_id": task.task_id,
445-
"name": task.name,
446-
"status": task.status.value,
447-
"created_at": task.created_at.isoformat(),
448-
"started_at": task.started_at.isoformat() if task.started_at else None,
449-
"completed_at": task.completed_at.isoformat() if task.completed_at else None,
450-
"retry_count": task.retry_count,
451-
"error": task.error,
452-
}
453-
454-
455435
@app.get("/tasks")
456436
async def list_tasks(status: str | None = None, limit: int = 50, _admin_key: AdminKeyDep = None) -> dict[str, Any]:
457437
"""List all tasks with optional status filter."""
@@ -498,6 +478,26 @@ async def worker_status(_admin_key: AdminKeyDep = None) -> dict[str, Any]:
498478
return {"workers": await _task_queue.worker_heartbeats()}
499479

500480

481+
@app.get("/tasks/{task_id}")
482+
async def get_task(task_id: str, _admin_key: AdminKeyDep = None) -> dict[str, Any]:
483+
"""Get task status by ID."""
484+
if not _task_queue:
485+
raise HTTPException(status_code=503, detail="Task queue not initialized")
486+
task = await _task_queue.get_task(task_id)
487+
if not task:
488+
raise HTTPException(status_code=404, detail="Task not found")
489+
return {
490+
"task_id": task.task_id,
491+
"name": task.name,
492+
"status": task.status.value,
493+
"created_at": task.created_at.isoformat(),
494+
"started_at": task.started_at.isoformat() if task.started_at else None,
495+
"completed_at": task.completed_at.isoformat() if task.completed_at else None,
496+
"retry_count": task.retry_count,
497+
"error": task.error,
498+
}
499+
500+
501501
@app.post("/prd/{plan_id}/versions")
502502
async def create_prd_version(
503503
plan_id: str,

tests/test_tasks_api.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import pytest
2+
from httpx import ASGITransport, AsyncClient
3+
4+
import app as app_module
5+
from agent_pm.api.auth import AdminKeyDep
6+
from agent_pm.settings import settings
7+
from app import app
8+
9+
10+
class AppClient:
11+
def __init__(self):
12+
self.transport = ASGITransport(app=app)
13+
self.client = AsyncClient(transport=self.transport, base_url="http://test")
14+
15+
async def __aenter__(self):
16+
await app_module.startup_event()
17+
return self.client
18+
19+
async def __aexit__(self, exc_type, exc, tb):
20+
await self.client.aclose()
21+
await app_module.shutdown_event()
22+
23+
24+
async def _create_client():
25+
return AppClient()
26+
27+
28+
@pytest.mark.asyncio
29+
async def test_tasks_admin_endpoints_with_memory_backend(monkeypatch):
30+
monkeypatch.setattr(settings, "task_queue_backend", "memory")
31+
app.dependency_overrides[AdminKeyDep] = lambda: "admin-test-key"
32+
33+
try:
34+
async with await _create_client() as client:
35+
response = await client.get("/tasks/dead-letter")
36+
assert response.status_code == 200
37+
assert response.json() == {"dead_letter": [], "total": 0}
38+
39+
worker_resp = await client.get("/tasks/workers")
40+
assert worker_resp.status_code == 200
41+
assert worker_resp.json() == {"workers": {}}
42+
finally:
43+
app.dependency_overrides.clear()
44+
45+
46+
class StubQueue:
47+
def __init__(self):
48+
self.limit: int | None = None
49+
self.deleted: str | None = None
50+
51+
async def list_dead_letters(self, limit: int = 100):
52+
self.limit = limit
53+
return [
54+
{
55+
"task_id": "dead-1",
56+
"name": "explode",
57+
"retry_count": 3,
58+
"last_error": "boom",
59+
}
60+
]
61+
62+
async def delete_dead_letter(self, task_id: str) -> None:
63+
self.deleted = task_id
64+
65+
async def worker_heartbeats(self) -> dict[str, dict[str, str]]:
66+
return {"worker:1": {"status": "ok"}}
67+
68+
async def get_task(self, task_id: str): # pragma: no cover - minimal stub for routing
69+
return None
70+
71+
72+
@pytest.mark.asyncio
73+
async def test_tasks_admin_endpoints_surface_queue_data(monkeypatch):
74+
monkeypatch.setattr(settings, "task_queue_backend", "memory")
75+
app.dependency_overrides[AdminKeyDep] = lambda: "admin-test-key"
76+
77+
try:
78+
async with await _create_client() as client:
79+
original_backend = settings.task_queue_backend
80+
original_queue = app_module._task_queue
81+
stub = StubQueue()
82+
app_module._task_queue = stub
83+
settings.task_queue_backend = "memory"
84+
try:
85+
dead_resp = await client.get("/tasks/dead-letter", params={"limit": 5})
86+
assert dead_resp.status_code == 200
87+
assert dead_resp.json()["dead_letter"][0]["task_id"] == "dead-1"
88+
assert stub.limit == 5
89+
90+
del_resp = await client.delete("/tasks/dead-letter/dead-1")
91+
assert del_resp.status_code == 200
92+
assert stub.deleted == "dead-1"
93+
94+
worker_resp = await client.get("/tasks/workers")
95+
assert worker_resp.status_code == 200
96+
assert worker_resp.json() == {"workers": {"worker:1": {"status": "ok"}}}
97+
finally:
98+
settings.task_queue_backend = original_backend
99+
app_module._task_queue = original_queue
100+
finally:
101+
app.dependency_overrides.clear()

0 commit comments

Comments
 (0)