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>
211 lines
7.2 KiB
Python
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
|