"""AP1.4 — RoadmapItem & Quality Gate tests.""" from __future__ import annotations from tests.factories import provision_user_in_tenant from tests.test_initiatives_actions import ( _auth, _create_initiative, _login, ) def _create_roadmap_item(client, token, initiative_id, **kwargs): body = {"title": "Gate A", "item_type": "milestone", **kwargs} return client.post( f"/api/initiatives/{initiative_id}/roadmap/items", json=body, headers=_auth(token), ) def test_roadmap_item_crud(client): user = provision_user_in_tenant(tenant_role="member") token = _login(client, user) initiative_id = _create_initiative(client, token).json()["id"] created = _create_roadmap_item( client, token, initiative_id, sequencing_mode="parallel", item_type="maturity_stage", ) assert created.status_code == 201 item = created.json() assert item["item_type"] == "maturity_stage" assert item["sequencing_mode"] == "parallel" listed = client.get( f"/api/initiatives/{initiative_id}/roadmap/items", headers=_auth(token), ) assert listed.status_code == 200 assert len(listed.json()) >= 1 patched = client.patch( f"/api/roadmap-items/{item['id']}", json={"status": "at_risk"}, headers=_auth(token), ) assert patched.status_code == 200 assert patched.json()["status"] == "at_risk" deleted = client.delete(f"/api/roadmap-items/{item['id']}", headers=_auth(token)) assert deleted.status_code == 204 def test_verify_reached_requires_evidence(client): user = provision_user_in_tenant(tenant_role="member") token = _login(client, user) initiative_id = _create_initiative(client, token).json()["id"] item = _create_roadmap_item(client, token, initiative_id, status="active").json() fail = client.post( f"/api/roadmap-items/{item['id']}/verify-reached", headers=_auth(token), ) assert fail.status_code == 400 evidence = client.post( f"/api/initiatives/{initiative_id}/evidence", json={ "title": "Nachweis", "roadmap_item_id": item["id"], "status": "accepted", }, headers=_auth(token), ) assert evidence.status_code == 201 ok = client.post( f"/api/roadmap-items/{item['id']}/verify-reached", headers=_auth(token), ) assert ok.status_code == 200 assert ok.json()["status"] == "reached" assert ok.json()["verify_reason"] in ("criteria_ready", "evidence_accepted") blocked_delete = client.delete( f"/api/roadmap-items/{item['id']}", headers=_auth(token), ) assert blocked_delete.status_code == 400 assert "nicht gelöscht" in blocked_delete.json()["detail"].lower() or "terminal" in blocked_delete.json()["detail"].lower() def test_milestone_compat_api_uses_roadmap(client): user = provision_user_in_tenant(tenant_role="member") token = _login(client, user) initiative_id = _create_initiative(client, token).json()["id"] created = client.post( f"/api/initiatives/{initiative_id}/milestones", json={"title": "Legacy MS", "status": "planned"}, headers=_auth(token), ) assert created.status_code == 201 ms_id = created.json()["id"] roadmap = client.get( f"/api/initiatives/{initiative_id}/roadmap/items", headers=_auth(token), ).json() assert any(i["id"] == ms_id and i["item_type"] == "milestone" for i in roadmap) def test_steering_snapshot_reads_roadmap_items(client): user = provision_user_in_tenant(tenant_role="member") token = _login(client, user) initiative_id = _create_initiative(client, token).json()["id"] _create_roadmap_item(client, token, initiative_id, status="active", target_date="2026-12-31") snap = client.get( f"/api/initiatives/{initiative_id}/steering-snapshot", headers=_auth(token), ).json() assert snap.get("roadmap_items") assert snap.get("upcoming_milestones") assert len(snap["upcoming_roadmap_items"]) >= 1 def test_initiative_roadmap_dependencies_list(client): user = provision_user_in_tenant(tenant_role="member") token = _login(client, user) initiative_id = _create_initiative(client, token).json()["id"] gate_a = _create_roadmap_item(client, token, initiative_id, title="Gate A").json() gate_b = _create_roadmap_item(client, token, initiative_id, title="Gate B").json() created = client.post( f"/api/roadmap-items/{gate_a['id']}/dependencies", json={"to_item_id": gate_b["id"], "dependency_type": "requires"}, headers=_auth(token), ) assert created.status_code == 201 listed = client.get( f"/api/initiatives/{initiative_id}/roadmap/dependencies", headers=_auth(token), ) assert listed.status_code == 200 deps = listed.json() assert len(deps) == 1 assert deps[0]["from_item_id"] == gate_a["id"] assert deps[0]["to_item_id"] == gate_b["id"] assert deps[0]["dependency_type"] == "requires" def test_roadmap_dependency_delete(client): user = provision_user_in_tenant(tenant_role="member") token = _login(client, user) initiative_id = _create_initiative(client, token).json()["id"] gate_a = _create_roadmap_item(client, token, initiative_id, title="Gate A").json() gate_b = _create_roadmap_item(client, token, initiative_id, title="Gate B").json() created = client.post( f"/api/roadmap-items/{gate_a['id']}/dependencies", json={"to_item_id": gate_b["id"], "dependency_type": "requires"}, headers=_auth(token), ) dep_id = created.json()["id"] deleted = client.delete( f"/api/roadmap-dependencies/{dep_id}", headers=_auth(token), ) assert deleted.status_code == 204 listed = client.get( f"/api/initiatives/{initiative_id}/roadmap/dependencies", headers=_auth(token), ) assert listed.json() == [] def test_roadmap_dependency_duplicate_rejected(client): user = provision_user_in_tenant(tenant_role="member") token = _login(client, user) initiative_id = _create_initiative(client, token).json()["id"] gate_a = _create_roadmap_item(client, token, initiative_id, title="Gate A").json() gate_b = _create_roadmap_item(client, token, initiative_id, title="Gate B").json() body = {"to_item_id": gate_b["id"], "dependency_type": "requires"} assert client.post( f"/api/roadmap-items/{gate_a['id']}/dependencies", json=body, headers=_auth(token), ).status_code == 201 dup = client.post( f"/api/roadmap-items/{gate_a['id']}/dependencies", json=body, headers=_auth(token), ) assert dup.status_code == 400 assert "existiert bereits" in dup.json()["detail"] def test_initiative_roadmap_criteria_progress(client): user = provision_user_in_tenant(tenant_role="member") token = _login(client, user) initiative_id = _create_initiative(client, token).json()["id"] item = _create_roadmap_item( client, token, initiative_id, title="Gate mit Kriterium", initial_criterion_title="Erstes Kriterium", ).json() progress = client.get( f"/api/initiatives/{initiative_id}/roadmap/criteria-progress", headers=_auth(token), ) assert progress.status_code == 200 body = progress.json() assert body[item["id"]]["total"] >= 1 assert body[item["id"]]["closed"] == 0 def test_initiative_roadmap_graph_state(client): user = provision_user_in_tenant(tenant_role="member") token = _login(client, user) initiative_id = _create_initiative(client, token).json()["id"] gate_a = _create_roadmap_item(client, token, initiative_id, title="Gate A", sort_order=0).json() gate_b = _create_roadmap_item(client, token, initiative_id, title="Gate B", sort_order=1).json() client.post( f"/api/roadmap-items/{gate_a['id']}/dependencies", json={"to_item_id": gate_b["id"], "dependency_type": "requires"}, headers=_auth(token), ) generic = client.get( f"/api/initiatives/{initiative_id}/roadmap/graph-state", headers=_auth(token), ) assert generic.status_code == 200 generic_body = generic.json() assert generic_body["graph_profile"]["enforce_gate_blocking"] is False assert generic_body["items"][gate_a["id"]]["would_block"] is True assert generic_body["items"][gate_a["id"]]["blocked"] is False client.patch( f"/api/steering/initiatives/{initiative_id}/context", json={"method_key": "product_milestone_driven"}, headers=_auth(token), ) state = client.get( f"/api/initiatives/{initiative_id}/roadmap/graph-state", headers=_auth(token), ) assert state.status_code == 200 body = state.json() assert body["graph_profile"]["enforce_gate_blocking"] is True assert body["items"][gate_a["id"]]["blocked"] is True assert gate_b["id"] in body["items"][gate_a["id"]]["blocked_by"] assert gate_a["id"] not in body["ready_items"] def test_graph_blocked_attention_for_gate_enforcing_method(client): user = provision_user_in_tenant(tenant_role="member") token = _login(client, user) initiative_id = _create_initiative(client, token).json()["id"] client.patch( f"/api/steering/initiatives/{initiative_id}/context", json={"method_key": "product_milestone_driven"}, headers=_auth(token), ) gate_a = _create_roadmap_item(client, token, initiative_id, title="Gate A", sort_order=0).json() gate_b = _create_roadmap_item(client, token, initiative_id, title="Gate B", sort_order=1).json() client.post( f"/api/roadmap-items/{gate_a['id']}/dependencies", json={"to_item_id": gate_b["id"], "dependency_type": "requires"}, headers=_auth(token), ) attention = client.get("/api/workspace/attention", headers=_auth(token)) assert attention.status_code == 200 assert any( i["kind"] == "gate_graph_blocked" and i["milestone_id"] == gate_a["id"] for i in attention.json() ) def test_plan_ist_view_and_snapshot(client): user = provision_user_in_tenant(tenant_role="member") token = _login(client, user) initiative_id = _create_initiative(client, token).json()["id"] gate = _create_roadmap_item(client, token, initiative_id, title="Gate Plan").json() view = client.get( f"/api/initiatives/{initiative_id}/roadmap/plan-ist-view", headers=_auth(token), ) assert view.status_code == 200 body = view.json() assert body["plan_source"] == "live" assert len(body["plan"]["items"]) >= 1 assert body["latest_snapshot"] is None snap = client.post( f"/api/initiatives/{initiative_id}/roadmap/plan-snapshots", json={"reason": "Baseline vor Umsetzung"}, headers=_auth(token), ) assert snap.status_code == 201 assert snap.json()["revision_number"] == 1 view2 = client.get( f"/api/initiatives/{initiative_id}/roadmap/plan-ist-view", headers=_auth(token), ) assert view2.status_code == 200 body2 = view2.json() assert body2["plan_source"] == "snapshot" assert body2["latest_snapshot"]["revision_number"] == 1 patched = client.patch( f"/api/roadmap-items/{gate['id']}", json={"title": "Gate umbenannt"}, headers=_auth(token), ) assert patched.status_code == 200 view3 = client.get( f"/api/initiatives/{initiative_id}/roadmap/plan-ist-view", headers=_auth(token), ) diff_kinds = {d["kind"] for d in view3.json()["diff"]} assert "gate_modified" in diff_kinds