"""AP2.2b — A2 linear project End-to-End (Neue Küche Happy Path).""" from __future__ import annotations from tests.factories import provision_user_in_tenant from tests.test_initiatives_actions import _auth, _create_initiative, _login _STARTER_ACTION = "Erster Schritt — Planung konkretisieren" _GATE_TITLES = ( "G1 — Planung", "G2 — Vorbereitung", "G3 — Umsetzung", "G4 — Abschluss", ) def _gate_by_title(items: list[dict], prefix: str) -> dict: return next(i for i in items if i.get("title", "").startswith(prefix)) def _create_kitchen(client, token): created = _create_initiative( client, token, title="Neue Küche", archetype_key="initiative.linear_project", ) assert created.status_code == 201 body = created.json() assert body["starter_kit"]["applied"] is True assert body["archetype_key"] == "initiative.linear_project" return body["id"] def test_a2_starter_kit_kitchen_structure(client): """Starter-Kit: Gates in Kette, Projects, Guidance, Operating Context.""" user = provision_user_in_tenant(tenant_role="admin") token = _login(client, user) initiative_id = _create_kitchen(client, token) ctx = client.get( f"/api/initiatives/{initiative_id}/operating-context", headers=_auth(token), ) assert ctx.status_code == 200 body = ctx.json() assert body["method_key"] == "sequential_dependency" assert "critical_path" in body["steering_elements"] assert "gate_fulfillment" in body["steering_elements"] assert body["ui_profile"]["planDefaultRoute"] == "/plan/gates" assert body["ui_profile"]["controlDefaultRoute"] == "/control/status" roadmap = client.get( f"/api/initiatives/{initiative_id}/roadmap/items", headers=_auth(token), ) assert roadmap.status_code == 200 items = roadmap.json() assert len(items) == 4 assert {i["title"] for i in items} == set(_GATE_TITLES) assert sum(1 for i in items if i["status"] == "active") == 1 assert _gate_by_title(items, "G1")["status"] == "active" deps = client.get( f"/api/initiatives/{initiative_id}/roadmap/dependencies", headers=_auth(token), ) assert deps.status_code == 200 gate_ids = {i["title"]: i["id"] for i in items} dep_pairs = {(d["from_item_id"], d["to_item_id"]) for d in deps.json()} assert (gate_ids["G2 — Vorbereitung"], gate_ids["G1 — Planung"]) in dep_pairs assert (gate_ids["G3 — Umsetzung"], gate_ids["G2 — Vorbereitung"]) in dep_pairs assert (gate_ids["G4 — Abschluss"], gate_ids["G3 — Umsetzung"]) in dep_pairs projects = client.get( f"/api/initiatives/{initiative_id}/projects", headers=_auth(token), ) assert projects.status_code == 200 assert {"Hauptpfad", "Begleitung"} <= {p["title"] for p in projects.json()} actions = client.get( f"/api/initiatives/{initiative_id}/actions", headers=_auth(token), ) assert actions.status_code == 200 assert any(a["title"] == _STARTER_ACTION for a in actions.json()) snap = client.get( f"/api/initiatives/{initiative_id}/steering-snapshot", headers=_auth(token), ) assert snap.status_code == 200 snap_body = snap.json() assert snap_body["steering_guidance"] assert snap_body["steering_kernel"]["primary_method_key"] == "sequential_dependency" assert snap_body["steering_kernel"]["horizon"]["kind"] == "gate" next_actions = snap_body.get("next_actions") or [] assert next_actions starter = next(a for a in actions.json() if a["title"] == _STARTER_ACTION) assert next_actions[0]["action_id"] == starter["id"] assert next_actions[0]["reason_code"] in ( "execution_ready", "execution_critical_path", ) def test_a2_kitchen_critical_path_complete_advances_next(client): """Montage-Kette am aktiven Gate: kritischer Pfad, Next mit Begründung, Fortschritt.""" user = provision_user_in_tenant(tenant_role="admin") token = _login(client, user) initiative_id = _create_kitchen(client, token) actions = client.get( f"/api/initiatives/{initiative_id}/actions", headers=_auth(token), ) starter = next(a for a in actions.json() if a["title"] == _STARTER_ACTION) deleted = client.delete( f"/api/actions/{starter['id']}", headers=_auth(token), ) assert deleted.status_code == 204 roadmap = client.get( f"/api/initiatives/{initiative_id}/roadmap/items", headers=_auth(token), ) g1_id = _gate_by_title(roadmap.json(), "G1")["id"] titles = ["Geräte geliefert", "Einbau Montage", "Endabnahme Küche"] action_ids: list[str] = [] for index, title in enumerate(titles): res = client.post( f"/api/initiatives/{initiative_id}/actions", json={ "title": title, "status": "open", "roadmap_item_id": g1_id, "sort_order": index, }, headers=_auth(token), ) assert res.status_code == 201 action_ids.append(res.json()["id"]) for pred, succ in zip(action_ids[:-1], action_ids[1:]): dep = client.post( f"/api/initiatives/{initiative_id}/execution/dependencies", json={ "predecessor_action_id": pred, "successor_action_id": succ, "dependency_kind": "requires", }, headers=_auth(token), ) assert dep.status_code == 201 graph = client.get( f"/api/initiatives/{initiative_id}/execution/graph-state", headers=_auth(token), ) assert graph.status_code == 200 graph_body = graph.json() assert graph_body["critical_path"] == action_ids assert action_ids[0] in graph_body["ready_actions"] assert action_ids[1] in graph_body["blocked_actions"] snap = client.get( f"/api/initiatives/{initiative_id}/steering-snapshot", headers=_auth(token), ) assert snap.status_code == 200 snap_body = snap.json() kernel = snap_body["steering_kernel"] exec_graph = kernel["read_models"].get("execution_graph") or {} assert exec_graph.get("critical_path") == action_ids next_actions = snap_body.get("next_actions") or [] assert next_actions top = next_actions[0] assert top["action_id"] == action_ids[0] assert top["reason_code"] in ("execution_ready", "execution_critical_path") done = client.patch( f"/api/actions/{action_ids[0]}", json={"status": "done"}, headers=_auth(token), ) assert done.status_code == 200 snap_after = client.get( f"/api/initiatives/{initiative_id}/steering-snapshot", headers=_auth(token), ) assert snap_after.status_code == 200 next_after = snap_after.json().get("next_actions") or [] assert next_after assert next_after[0]["action_id"] == action_ids[1] assert next_after[0]["reason_code"] in ( "execution_ready", "execution_critical_path", ) def test_a2_kitchen_planning_debt_surfaces_gate_proposals(client): """Aktives Gate ohne Actions → Planning Debt + gate_next_actions Proposal.""" user = provision_user_in_tenant(tenant_role="admin") token = _login(client, user) initiative_id = _create_kitchen(client, token) actions = client.get( f"/api/initiatives/{initiative_id}/actions", headers=_auth(token), ) starter = next(a for a in actions.json() if a["title"] == _STARTER_ACTION) deleted = client.delete( f"/api/actions/{starter['id']}", headers=_auth(token), ) assert deleted.status_code == 204 snap = client.get( f"/api/initiatives/{initiative_id}/steering-snapshot", headers=_auth(token), ) assert snap.status_code == 200 snap_body = snap.json() planning_debt = snap_body["steering_kernel"]["read_models"].get("planning_debt") or [] assert planning_debt assert any(d.get("kind") == "missing_execution_plan" for d in planning_debt) gate_proposals = snap_body.get("gate_next_actions_proposals") or [] assert gate_proposals assert any(p.get("reason_code") == "planning_debt" for p in gate_proposals) attention_codes = {a.get("code") for a in snap_body.get("attention_items") or []} assert "planning_debt" in attention_codes