generated from coulomb/repo-seed
Infrastructure (T01): - tests/conftest.py: sync schema setup (psycopg2), per-test table truncation, async ASGI client with get_session override - pyproject.toml: [tool.pytest.ini_options] asyncio_mode=auto - Makefile: make test target with TEST_DATABASE_URL Core router tests (T02): 19 tests - domains, topics, workstreams, tasks, decisions + state summary - Caught real bug: topic router missing duplicate-slug 409 guard (fixed) TD/EP/Contributions/SBOM tests (T03): 10 tests - CRUD + status transitions + lifecycle guard + SBOM ingest MCP smoke tests (T04): 12 tests - get_state_summary, create_task, update_task_status, add_progress_event, flag_for_human HTTP shapes CI gate (T05): make test documented in CLAUDE.md session protocol Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
277 lines
10 KiB
Python
277 lines
10 KiB
Python
"""
|
|
Core router tests: topics, domains, workstreams, tasks, decisions, state summary.
|
|
|
|
Happy path + key error cases. All tests use a real PostgreSQL test database
|
|
(no mocking). The `client` fixture provides an httpx.AsyncClient backed by
|
|
the FastAPI ASGI app.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
async def _create_domain(client, slug="testdomain", name="Test Domain"):
|
|
r = await client.post("/domains/", json={"slug": slug, "name": name})
|
|
assert r.status_code == 201, r.text
|
|
return r.json()
|
|
|
|
|
|
async def _create_topic(client, domain_slug="testdomain", slug="testtopic", title="Test Topic"):
|
|
r = await client.post("/topics/", json={
|
|
"slug": slug, "title": title, "domain": domain_slug,
|
|
})
|
|
assert r.status_code == 201, r.text
|
|
return r.json()
|
|
|
|
|
|
async def _create_workstream(client, topic_id, slug="test-ws", title="Test WS"):
|
|
r = await client.post("/workstreams/", json={
|
|
"topic_id": topic_id, "slug": slug, "title": title,
|
|
})
|
|
assert r.status_code == 201, r.text
|
|
return r.json()
|
|
|
|
|
|
async def _create_task(client, workstream_id, title="Test task"):
|
|
r = await client.post("/tasks/", json={
|
|
"workstream_id": workstream_id, "title": title,
|
|
})
|
|
assert r.status_code == 201, r.text
|
|
return r.json()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Domain tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestDomains:
|
|
async def test_create_and_list(self, client):
|
|
await _create_domain(client)
|
|
r = await client.get("/domains/")
|
|
assert r.status_code == 200
|
|
slugs = [d["slug"] for d in r.json()]
|
|
assert "testdomain" in slugs
|
|
|
|
async def test_duplicate_slug_returns_409(self, client):
|
|
await _create_domain(client)
|
|
r = await client.post("/domains/", json={"slug": "testdomain", "name": "Dupe"})
|
|
assert r.status_code == 409
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Topic tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestTopics:
|
|
async def test_create_and_get(self, client):
|
|
await _create_domain(client)
|
|
topic = await _create_topic(client)
|
|
r = await client.get(f"/topics/{topic['id']}")
|
|
assert r.status_code == 200
|
|
assert r.json()["slug"] == "testtopic"
|
|
|
|
async def test_duplicate_slug_returns_409(self, client):
|
|
await _create_domain(client)
|
|
await _create_topic(client)
|
|
r = await client.post("/topics/", json={
|
|
"slug": "testtopic", "title": "Dupe", "domain": "testdomain",
|
|
})
|
|
assert r.status_code == 409
|
|
|
|
async def test_unknown_domain_returns_404(self, client):
|
|
r = await client.post("/topics/", json={
|
|
"slug": "x", "title": "X", "domain": "doesnotexist",
|
|
})
|
|
assert r.status_code == 404
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Workstream tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestWorkstreams:
|
|
async def test_create_and_list_by_topic(self, client):
|
|
await _create_domain(client)
|
|
topic = await _create_topic(client)
|
|
ws = await _create_workstream(client, topic["id"])
|
|
|
|
r = await client.get(f"/workstreams/?topic_id={topic['id']}")
|
|
assert r.status_code == 200
|
|
ids = [w["id"] for w in r.json()]
|
|
assert ws["id"] in ids
|
|
|
|
async def test_status_transition(self, client):
|
|
await _create_domain(client)
|
|
topic = await _create_topic(client)
|
|
ws = await _create_workstream(client, topic["id"])
|
|
|
|
r = await client.patch(f"/workstreams/{ws['id']}", json={"status": "completed"})
|
|
assert r.status_code == 200
|
|
assert r.json()["status"] == "completed"
|
|
|
|
async def test_filter_by_owner(self, client):
|
|
await _create_domain(client)
|
|
topic = await _create_topic(client)
|
|
ws = await _create_workstream(client, topic["id"])
|
|
await client.patch(f"/workstreams/{ws['id']}", json={"owner": "alice"})
|
|
|
|
r = await client.get("/workstreams/?owner=alice")
|
|
assert r.status_code == 200
|
|
assert len(r.json()) == 1
|
|
assert r.json()[0]["id"] == ws["id"]
|
|
|
|
async def test_filter_by_slug(self, client):
|
|
await _create_domain(client)
|
|
topic = await _create_topic(client)
|
|
ws = await _create_workstream(client, topic["id"], slug="my-special-ws")
|
|
|
|
r = await client.get("/workstreams/?slug=my-special-ws")
|
|
assert r.status_code == 200
|
|
assert len(r.json()) == 1
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Task tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestTasks:
|
|
async def test_create_and_list_by_workstream(self, client):
|
|
await _create_domain(client)
|
|
topic = await _create_topic(client)
|
|
ws = await _create_workstream(client, topic["id"])
|
|
task = await _create_task(client, ws["id"])
|
|
|
|
r = await client.get(f"/tasks/?workstream_id={ws['id']}")
|
|
assert r.status_code == 200
|
|
ids = [t["id"] for t in r.json()]
|
|
assert task["id"] in ids
|
|
|
|
async def test_needs_human_flag(self, client):
|
|
await _create_domain(client)
|
|
topic = await _create_topic(client)
|
|
ws = await _create_workstream(client, topic["id"])
|
|
task = await _create_task(client, ws["id"])
|
|
|
|
r = await client.patch(f"/tasks/{task['id']}", json={
|
|
"needs_human": True,
|
|
"intervention_note": "needs review",
|
|
})
|
|
assert r.status_code == 200
|
|
assert r.json()["needs_human"] is True
|
|
|
|
async def test_needs_human_without_note_returns_422(self, client):
|
|
await _create_domain(client)
|
|
topic = await _create_topic(client)
|
|
ws = await _create_workstream(client, topic["id"])
|
|
task = await _create_task(client, ws["id"])
|
|
|
|
r = await client.patch(f"/tasks/{task['id']}", json={"needs_human": True})
|
|
assert r.status_code == 422
|
|
|
|
async def test_cancel_via_delete(self, client):
|
|
await _create_domain(client)
|
|
topic = await _create_topic(client)
|
|
ws = await _create_workstream(client, topic["id"])
|
|
task = await _create_task(client, ws["id"])
|
|
|
|
r = await client.delete(f"/tasks/{task['id']}")
|
|
assert r.status_code == 200
|
|
assert r.json()["status"] == "cancelled"
|
|
|
|
async def test_filter_by_priority(self, client):
|
|
await _create_domain(client)
|
|
topic = await _create_topic(client)
|
|
ws = await _create_workstream(client, topic["id"])
|
|
await client.post("/tasks/", json={
|
|
"workstream_id": ws["id"], "title": "High prio", "priority": "high",
|
|
})
|
|
await client.post("/tasks/", json={
|
|
"workstream_id": ws["id"], "title": "Low prio", "priority": "low",
|
|
})
|
|
|
|
r = await client.get(f"/tasks/?workstream_id={ws['id']}&priority=high")
|
|
assert r.status_code == 200
|
|
titles = [t["title"] for t in r.json()]
|
|
assert "High prio" in titles
|
|
assert "Low prio" not in titles
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Decision tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestDecisions:
|
|
async def test_create_and_resolve(self, client):
|
|
await _create_domain(client)
|
|
topic = await _create_topic(client)
|
|
r = await client.post("/decisions/", json={
|
|
"title": "Should we use X?",
|
|
"topic_id": topic["id"],
|
|
})
|
|
assert r.status_code == 201
|
|
d_id = r.json()["id"]
|
|
|
|
r2 = await client.post(f"/decisions/{d_id}/resolve", json={
|
|
"rationale": "Yes, use X.",
|
|
"decided_by": "bernd",
|
|
})
|
|
assert r2.status_code == 200
|
|
assert r2.json()["status"] == "resolved"
|
|
|
|
async def test_resolve_already_resolved_returns_409(self, client):
|
|
await _create_domain(client)
|
|
topic = await _create_topic(client)
|
|
r = await client.post("/decisions/", json={
|
|
"title": "Already done", "topic_id": topic["id"],
|
|
})
|
|
d_id = r.json()["id"]
|
|
await client.post(f"/decisions/{d_id}/resolve", json={"rationale": "Done", "decided_by": "bernd"})
|
|
|
|
r2 = await client.post(f"/decisions/{d_id}/resolve", json={"rationale": "Again", "decided_by": "bernd"})
|
|
assert r2.status_code == 409
|
|
|
|
async def test_financial_keyword_auto_escalates(self, client):
|
|
await _create_domain(client)
|
|
topic = await _create_topic(client)
|
|
r = await client.post("/decisions/", json={
|
|
"title": "Purchase cloud credits",
|
|
"topic_id": topic["id"],
|
|
"decision_type": "pending",
|
|
})
|
|
assert r.status_code == 201
|
|
body = r.json()
|
|
assert body["status"] == "escalated"
|
|
assert body["escalation_note"]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# State summary
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestStateSummary:
|
|
async def test_summary_returns_expected_shape(self, client):
|
|
r = await client.get("/state/summary")
|
|
assert r.status_code == 200
|
|
body = r.json()
|
|
assert "open_workstreams" in body
|
|
assert "blocking_decisions" in body
|
|
assert "blocked_tasks" in body
|
|
assert "domains" in body
|
|
|
|
async def test_summary_counts_reflect_created_data(self, client):
|
|
await _create_domain(client)
|
|
topic = await _create_topic(client)
|
|
ws = await _create_workstream(client, topic["id"])
|
|
task = await _create_task(client, ws["id"])
|
|
# Mark as blocked so it shows in blocked_tasks
|
|
await client.patch(f"/tasks/{task['id']}",
|
|
json={"status": "blocked", "blocking_reason": "waiting on dep"})
|
|
|
|
r = await client.get("/state/summary")
|
|
body = r.json()
|
|
assert len(body["blocked_tasks"]) >= 1
|