Files
state-hub/tests/test_routers_td_ep_contrib.py
tegwick 75d25e9d3b feat(tests): pytest-asyncio test suite — 119 tests across 3 modules
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>
2026-03-18 12:00:06 +01:00

211 lines
7.2 KiB
Python

"""
Router tests: technical debt, extension points, contributions, SBOM ingest.
"""
from __future__ import annotations
import pytest
# ---------------------------------------------------------------------------
# Helpers (reuse domain/topic setup)
# ---------------------------------------------------------------------------
async def _setup(client):
"""Create a domain + topic; return (domain_slug, topic_id)."""
r = await client.post("/domains/", json={"slug": "testdom", "name": "Test Domain"})
assert r.status_code == 201
r2 = await client.post("/topics/", json={
"slug": "testtopic", "title": "Test Topic", "domain": "testdom",
})
assert r2.status_code == 201
return "testdom", r2.json()["id"]
async def _create_repo(client, domain_slug, slug="testrepo"):
r = await client.post("/repos/", json={
"slug": slug,
"name": "Test Repo",
"domain_slug": domain_slug,
"local_path": "/tmp/testrepo",
})
assert r.status_code == 201, r.text
return r.json()
# ---------------------------------------------------------------------------
# Technical Debt
# ---------------------------------------------------------------------------
class TestTechnicalDebt:
async def test_create_and_list(self, client):
domain_slug, _ = await _setup(client)
r = await client.post("/technical-debt/", json={
"title": "Use UTC datetimes", "domain": domain_slug,
})
assert r.status_code == 201
td_id = r.json()["id"]
r2 = await client.get(f"/technical-debt/?domain={domain_slug}")
assert r2.status_code == 200
ids = [t["id"] for t in r2.json()]
assert td_id in ids
async def test_status_transition(self, client):
domain_slug, _ = await _setup(client)
r = await client.post("/technical-debt/", json={
"title": "Old debt", "domain": domain_slug,
})
td_id = r.json()["id"]
r2 = await client.patch(f"/technical-debt/{td_id}", json={"status": "resolved"})
assert r2.status_code == 200
assert r2.json()["status"] == "resolved"
async def test_add_note(self, client):
domain_slug, _ = await _setup(client)
r = await client.post("/technical-debt/", json={
"title": "Needs note", "domain": domain_slug,
})
td_id = r.json()["id"]
r2 = await client.post(f"/technical-debt/{td_id}/notes/", json={
"step": "analysis",
"content": "Root cause identified.",
})
assert r2.status_code == 201
r3 = await client.get(f"/technical-debt/{td_id}/notes/")
assert r3.status_code == 200
assert len(r3.json()) == 1
# ---------------------------------------------------------------------------
# Extension Points
# ---------------------------------------------------------------------------
class TestExtensionPoints:
async def test_create_and_list(self, client):
domain_slug, _ = await _setup(client)
r = await client.post("/extension-points/", json={
"domain": domain_slug,
"title": "Auth hook",
"ep_type": "hook",
})
assert r.status_code == 201
ep_id = r.json()["id"]
r2 = await client.get(f"/extension-points/?domain={domain_slug}")
assert r2.status_code == 200
ids = [e["id"] for e in r2.json()]
assert ep_id in ids
async def test_status_transition(self, client):
domain_slug, _ = await _setup(client)
r = await client.post("/extension-points/", json={
"domain": domain_slug, "title": "Webhook", "ep_type": "webhook",
})
ep_id = r.json()["id"]
r2 = await client.patch(f"/extension-points/{ep_id}", json={"status": "addressed"})
assert r2.status_code == 200
assert r2.json()["status"] == "addressed"
# ---------------------------------------------------------------------------
# Contributions
# ---------------------------------------------------------------------------
class TestContributions:
async def test_create_draft_and_submit(self, client):
r = await client.post("/contributions/", json={
"type": "br",
"title": "Fix null pointer",
"target_org": "anthropics",
"target_repo": "sdk",
})
assert r.status_code == 201
c_id = r.json()["id"]
assert r.json()["status"] == "draft"
r2 = await client.patch(f"/contributions/{c_id}/status", json={
"status": "submitted",
})
assert r2.status_code == 200
assert r2.json()["status"] == "submitted"
async def test_invalid_transition_returns_422(self, client):
r = await client.post("/contributions/", json={
"type": "br",
"title": "Fix crash",
"target_org": "anthropics",
"target_repo": "sdk",
})
c_id = r.json()["id"] # starts as draft
# draft → merged is not allowed (must go through submitted first)
r2 = await client.patch(f"/contributions/{c_id}/status", json={
"status": "merged",
})
assert r2.status_code == 422
async def test_list_by_type(self, client):
await client.post("/contributions/", json={
"type": "br", "title": "Bug A",
"target_org": "org", "target_repo": "repo",
})
await client.post("/contributions/", json={
"type": "fr", "title": "Feature B",
"target_org": "org", "target_repo": "repo",
})
r = await client.get("/contributions/?type=br")
assert r.status_code == 200
types = [c["type"] for c in r.json()]
assert all(t == "br" for t in types)
assert len(types) == 1
# ---------------------------------------------------------------------------
# SBOM ingest
# ---------------------------------------------------------------------------
class TestSBOM:
async def test_ingest_and_list_snapshots(self, client):
domain_slug, _ = await _setup(client)
await _create_repo(client, domain_slug)
r = await client.post("/sbom/ingest/", json={
"repo_slug": "testrepo",
"entries": [
{
"package_name": "fastapi",
"package_version": "0.115.0",
"ecosystem": "python",
"license_spdx": "MIT",
"is_direct": True,
"is_dev": False,
},
{
"package_name": "sqlalchemy",
"package_version": "2.0.0",
"ecosystem": "python",
"license_spdx": "MIT",
"is_direct": True,
"is_dev": False,
},
],
})
assert r.status_code == 200
assert r.json()["ingested"] == 2
r2 = await client.get("/sbom/snapshots/?repo_slug=testrepo")
assert r2.status_code == 200
assert len(r2.json()) == 1
async def test_ingest_unknown_repo_returns_404(self, client):
r = await client.post("/sbom/ingest/", json={
"repo_slug": "doesnotexist",
"entries": [],
})
assert r.status_code == 404