Some checks failed
Deploy Development / deploy (push) Successful in 47s
Test Suite / pytest-backend (push) Failing after 2m18s
Test Suite / k6 /api/health Baseline (push) Has been skipped
Test Suite / playwright-smoke (push) Has been skipped
Test Suite / lint-backend (push) Successful in 3s
Test Suite / compose-smoke (push) Has been skipped
Revisionen mit Audit, getrennte Soll/Ist/Diff-Ansicht unter Kontrolle/Plan-Ist; API plan-ist-view und plan-snapshots. Co-authored-by: Cursor <cursoragent@cursor.com>
320 lines
11 KiB
Python
320 lines
11 KiB
Python
"""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")
|
|
|
|
|
|
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),
|
|
)
|
|
|
|
state = client.get(
|
|
f"/api/initiatives/{initiative_id}/roadmap/graph-state",
|
|
headers=_auth(token),
|
|
)
|
|
assert state.status_code == 200
|
|
body = state.json()
|
|
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
|