""" 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", status="active"): r = await client.post("/workstreams/", json={ "topic_id": topic_id, "slug": slug, "title": title, "status": status, }) 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": "finished"}) assert r.status_code == 200 assert r.json()["status"] == "finished" async def test_legacy_completed_status_is_normalized(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"] == "finished" 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 async def test_workplan_index_route(self, client): r = await client.get("/workstreams/workplan-index") assert r.status_code == 200 assert "workstreams" in r.json() # --------------------------------------------------------------------------- # 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 @pytest.mark.parametrize("initial_status", ["proposed", "ready", "backlog"]) async def test_task_start_activates_planning_workstream(self, client, initial_status): await _create_domain(client) topic = await _create_topic(client) ws = await _create_workstream( client, topic["id"], slug=f"{initial_status}-ws", status=initial_status, ) task = await _create_task(client, ws["id"]) r = await client.patch(f"/tasks/{task['id']}", json={"status": "in_progress"}) assert r.status_code == 200 r = await client.get(f"/workstreams/{ws['id']}") assert r.status_code == 200 assert r.json()["status"] == "active" async def test_task_start_does_not_unblock_blocked_workstream(self, client): await _create_domain(client) topic = await _create_topic(client) ws = await _create_workstream(client, topic["id"], slug="blocked-ws", status="blocked") task = await _create_task(client, ws["id"]) r = await client.patch(f"/tasks/{task['id']}", json={"status": "in_progress"}) assert r.status_code == 200 r = await client.get(f"/workstreams/{ws['id']}") assert r.status_code == 200 assert r.json()["status"] == "blocked" # --------------------------------------------------------------------------- # 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 async def test_summary_derives_blocked_workstream_from_flow_engine(self, client): await _create_domain(client) topic = await _create_topic(client) blocked_ws = await _create_workstream(client, topic["id"], slug="blocked-ws") dependency_ws = await _create_workstream(client, topic["id"], slug="dependency-ws") r = await client.post( f"/workstreams/{blocked_ws['id']}/dependencies/", json={ "to_workstream_id": dependency_ws["id"], "description": "Blocked until dependency completes", }, ) assert r.status_code == 201 r = await client.get("/state/summary") assert r.status_code == 200 body = r.json() summaries = {item["id"]: item for item in body["open_workstreams"]} assert summaries[blocked_ws["id"]]["status"] == "blocked" assert summaries[blocked_ws["id"]]["blocked_reasons"][0]["id"] == "dependencies.all_complete" assert body["totals"]["workstreams"]["blocked"] == 1 class TestFlowEndpoints: async def test_list_flow_definitions(self, client): r = await client.get("/flows/definitions") assert r.status_code == 200 entity_types = {item["entity_type"] for item in r.json()} assert {"workstream", "task", "contribution", "capability_request"} <= entity_types async def test_get_flow_state_and_advance_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"]) await client.patch(f"/tasks/{task['id']}", json={"status": "done"}) r = await client.get(f"/flows/workstream/{ws['id']}") assert r.status_code == 200 assert "finished" in r.json()["reachable"] r = await client.post(f"/flows/workstream/{ws['id']}/advance/finished") assert r.status_code == 200 assert r.json()["current_workstation"] == "finished" r = await client.get(f"/workstreams/{ws['id']}") assert r.json()["status"] == "finished" async def test_advance_workstream_respects_current_exit_assertions(self, client): await _create_domain(client) topic = await _create_topic(client) ws = await _create_workstream(client, topic["id"], slug="exit-blocked-ws") dependency_ws = await _create_workstream(client, topic["id"], slug="unfinished-dep") task = await _create_task(client, ws["id"]) await client.patch(f"/tasks/{task['id']}", json={"status": "done"}) await client.post( f"/workstreams/{ws['id']}/dependencies/", json={ "to_workstream_id": dependency_ws["id"], "description": "Dependency must finish first", }, ) r = await client.post(f"/flows/workstream/{ws['id']}/advance/finished") assert r.status_code == 409 assert r.json()["detail"]["blocking_assertions"][0]["id"] == "dependencies.all_complete" r = await client.get(f"/workstreams/{ws['id']}") assert r.json()["status"] == "active"