"""CUST-WP-0074-T02: derived wait_kind / blocked_kind read-model fields and the from_task_id dependency edge. Real PostgreSQL via the shared `client` fixture.""" from __future__ import annotations import pytest from api.services.wait_kind import derive_blocked_kind, derive_wait_kind from tests.conftest import create_test_domain, create_test_repo, create_test_workplan class TestDeriveFunctions: def test_wait_kind_by_qualifiers(self): assert derive_wait_kind("todo", needs_human=True, has_dependency=True) is None assert derive_wait_kind("wait", needs_human=False, has_dependency=False) == "unqualified" assert derive_wait_kind("wait", needs_human=False, has_dependency=True) == "external" assert derive_wait_kind("wait", needs_human=True, has_dependency=False) == "human" assert derive_wait_kind("wait", needs_human=True, has_dependency=True) == "both" def test_blocked_kind_prefers_human(self): assert derive_blocked_kind("active", ["human"]) is None assert derive_blocked_kind("blocked", []) == "none" assert derive_blocked_kind("blocked", ["unqualified", None]) == "none" assert derive_blocked_kind("blocked", ["external", "unqualified"]) == "external" assert derive_blocked_kind("blocked", ["external", "human"]) == "human" assert derive_blocked_kind("blocked", ["both"]) == "human" async def _task(client, workplan_id, title, **extra): r = await client.post("/tasks/", json={"workplan_id": workplan_id, "title": title, **extra}) assert r.status_code == 201, r.text return r.json() @pytest.mark.asyncio async def test_wait_kind_and_blocked_kind_on_read_routes(client): domain = await create_test_domain(client) repo = await create_test_repo(client, domain_slug=domain["slug"]) upstream = await create_test_workplan(client, repo["id"], slug="upstream", status="active") blocked = await create_test_workplan(client, repo["id"], slug="blocked", status="blocked") human = await _task( client, blocked["id"], "human gate", status="wait", needs_human=True, blocking_reason="operator must provision creds", intervention_note="operator must provision creds", decision_id="CCR-2026-0004", ) external = await _task( client, blocked["id"], "external", status="wait", blocking_reason="upstream first", ) unqualified = await _task(client, blocked["id"], "unqualified", status="wait", blocking_reason="?") todo = await _task(client, blocked["id"], "plain todo") r = await client.post( f"/workplans/{blocked['id']}/dependencies/", json={"from_task_id": external["id"], "to_workplan_id": upstream["id"]}, ) assert r.status_code == 201, r.text assert r.json()["from_task_id"] == external["id"] r = await client.get("/tasks/", params={"workplan_id": blocked["id"]}) assert r.status_code == 200 kinds = {row["title"]: row["wait_kind"] for row in r.json()} assert kinds == { "human gate": "human", "external": "external", "unqualified": "unqualified", "plain todo": None, } r = await client.get(f"/tasks/{human['id']}") assert r.json()["wait_kind"] == "human" assert r.json()["decision_id"] == "CCR-2026-0004" r = await client.get(f"/workplans/{blocked['id']}") assert r.json()["blocked_kind"] == "human" r = await client.get(f"/workplans/{upstream['id']}") assert r.json()["blocked_kind"] is None # Human gate lifted → the workplan is blocked on the external wait only. r = await client.patch(f"/tasks/{human['id']}", json={"status": "done"}) assert r.status_code == 200, r.text r = await client.get("/workplans/", params={"repo_id": repo["id"]}) kinds = {row["slug"]: row["blocked_kind"] for row in r.json()} assert kinds == {"upstream": None, "blocked": "external"} # Dependency and human flag together → both. r = await client.patch( f"/tasks/{external['id']}", json={"needs_human": True, "intervention_note": "confirm with owner"}, ) assert r.status_code == 200, r.text r = await client.get(f"/tasks/{external['id']}") assert r.json()["wait_kind"] == "both" del unqualified, todo @pytest.mark.asyncio async def test_from_task_must_belong_to_the_from_workplan(client): domain = await create_test_domain(client) repo = await create_test_repo(client, domain_slug=domain["slug"]) a = await create_test_workplan(client, repo["id"], slug="a") b = await create_test_workplan(client, repo["id"], slug="b") task_in_b = await _task(client, b["id"], "in b") r = await client.post( f"/workplans/{a['id']}/dependencies/", json={"from_task_id": task_in_b["id"], "to_workplan_id": b["id"]}, ) assert r.status_code == 404 r = await client.post( f"/workplans/{b['id']}/dependencies/", json={"from_task_id": task_in_b["id"], "to_task_id": task_in_b["id"]}, ) assert r.status_code == 422 @pytest.mark.asyncio async def test_two_tasks_may_wait_on_the_same_target(client): domain = await create_test_domain(client) repo = await create_test_repo(client, domain_slug=domain["slug"]) upstream = await create_test_workplan(client, repo["id"], slug="upstream") wp = await create_test_workplan(client, repo["id"], slug="waiting") t1 = await _task(client, wp["id"], "t1") t2 = await _task(client, wp["id"], "t2") for task in (t1, t2): r = await client.post( f"/workplans/{wp['id']}/dependencies/", json={"from_task_id": task["id"], "to_workplan_id": upstream["id"]}, ) assert r.status_code == 201, r.text r = await client.get(f"/workplans/{wp['id']}/dependencies/") assert sorted(row["from_task_id"] for row in r.json()) == sorted([t1["id"], t2["id"]])