All checks were successful
Deploy Development / deploy (push) Successful in 47s
Test Suite / pytest-backend (push) Successful in 4m24s
Test Suite / lint-backend (push) Successful in 2s
Test Suite / compose-smoke (push) Has been skipped
Test Suite / k6 /api/health Baseline (push) Successful in 18s
Test Suite / playwright-smoke (push) Successful in 12s
AP2.2b: Integrationstests Neue-Kueche-Happy-Path, Truth Table und Execution Plan abgeschlossen. UX Composition Kernel (Control/Plan/Cockpit-Slots), CriticalPathPanel aus Kernel-Read-Model mit Gate-Horizont, Next Action elementgesteuert ohne Snapshot-Doppelung, Product-Continuous-Copy und Plan/Arbeit ueber Kernel planning_debt. Co-authored-by: Cursor <cursoragent@cursor.com>
239 lines
8.3 KiB
Python
239 lines
8.3 KiB
Python
"""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
|