Some checks failed
Deploy Development / deploy (push) Successful in 45s
Test Suite / pytest-backend (push) Failing after 3m33s
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
list_compatible_methods nur Primary-Methoden; Agile-Graph auf Sprint-Scope; Tests ohne Starter-Action-Kollision; TenantContext-Factory fuer Kernel-Tests. Co-authored-by: Cursor <cursoragent@cursor.com>
211 lines
6.5 KiB
Python
211 lines
6.5 KiB
Python
"""AP2.2c — A2 sequential_dependency: Gate-Horizont + Agile-Komposition."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from tests.factories import provision_user_in_tenant
|
|
from tests.test_initiatives_actions import _auth, _create_initiative, _login
|
|
|
|
|
|
_STARTER_ACTION_TITLE = "Erster Schritt — Planung konkretisieren"
|
|
|
|
|
|
def _remove_starter_action(client, token, initiative_id):
|
|
actions = client.get(
|
|
f"/api/initiatives/{initiative_id}/actions",
|
|
headers=_auth(token),
|
|
)
|
|
assert actions.status_code == 200
|
|
for action in actions.json():
|
|
if action.get("title") == _STARTER_ACTION_TITLE:
|
|
deleted = client.delete(
|
|
f"/api/actions/{action['id']}",
|
|
headers=_auth(token),
|
|
)
|
|
assert deleted.status_code == 204
|
|
return
|
|
|
|
|
|
def _gate_ids(client, token, initiative_id):
|
|
roadmap = client.get(
|
|
f"/api/initiatives/{initiative_id}/roadmap/items",
|
|
headers=_auth(token),
|
|
)
|
|
assert roadmap.status_code == 200
|
|
items = roadmap.json()
|
|
active = next(i for i in items if i.get("status") == "active")
|
|
planned = next(i for i in items if i.get("title", "").startswith("G2"))
|
|
return active["id"], planned["id"]
|
|
|
|
|
|
def test_sequential_next_action_only_in_active_gate_horizon(client):
|
|
user = provision_user_in_tenant(tenant_role="admin")
|
|
token = _login(client, user)
|
|
|
|
created = _create_initiative(
|
|
client,
|
|
token,
|
|
title="Horizon Test",
|
|
archetype_key="initiative.linear_project",
|
|
)
|
|
initiative_id = created.json()["id"]
|
|
g1_id, g2_id = _gate_ids(client, token, initiative_id)
|
|
_remove_starter_action(client, token, initiative_id)
|
|
|
|
off_horizon = client.post(
|
|
f"/api/initiatives/{initiative_id}/actions",
|
|
json={
|
|
"title": "Außerhalb Horizont",
|
|
"status": "open",
|
|
"roadmap_item_id": g2_id,
|
|
"sort_order": 0,
|
|
},
|
|
headers=_auth(token),
|
|
)
|
|
on_horizon = client.post(
|
|
f"/api/initiatives/{initiative_id}/actions",
|
|
json={
|
|
"title": "Im aktiven Gate",
|
|
"status": "open",
|
|
"roadmap_item_id": g1_id,
|
|
"sort_order": 1,
|
|
},
|
|
headers=_auth(token),
|
|
)
|
|
assert off_horizon.status_code == 201 and on_horizon.status_code == 201
|
|
|
|
snap = client.get(
|
|
f"/api/initiatives/{initiative_id}/steering-snapshot",
|
|
headers=_auth(token),
|
|
)
|
|
assert snap.status_code == 200
|
|
next_actions = snap.json().get("next_actions") or []
|
|
assert next_actions
|
|
next_ids = {n.get("action_id") for n in next_actions}
|
|
assert off_horizon.json()["id"] not in next_ids
|
|
assert on_horizon.json()["id"] in next_ids
|
|
|
|
|
|
def test_agile_on_linear_delegates_primary_without_active_sprint(client):
|
|
user = provision_user_in_tenant(tenant_role="admin")
|
|
token = _login(client, user)
|
|
|
|
created = _create_initiative(
|
|
client,
|
|
token,
|
|
title="Agile Primary",
|
|
archetype_key="initiative.linear_project",
|
|
)
|
|
initiative_id = created.json()["id"]
|
|
g1_id, _ = _gate_ids(client, token, initiative_id)
|
|
_remove_starter_action(client, token, initiative_id)
|
|
|
|
action = client.post(
|
|
f"/api/initiatives/{initiative_id}/actions",
|
|
json={
|
|
"title": "Gate Action",
|
|
"status": "open",
|
|
"roadmap_item_id": g1_id,
|
|
},
|
|
headers=_auth(token),
|
|
)
|
|
assert action.status_code == 201
|
|
|
|
snap = client.get(
|
|
f"/api/initiatives/{initiative_id}/steering-snapshot",
|
|
headers=_auth(token),
|
|
)
|
|
assert snap.status_code == 200
|
|
assert snap.json().get("active_work_cycle") is None
|
|
next_actions = snap.json().get("next_actions") or []
|
|
assert next_actions[0]["action_id"] == action.json()["id"]
|
|
assert next_actions[0]["reason_code"] in (
|
|
"execution_ready",
|
|
"execution_critical_path",
|
|
)
|
|
|
|
|
|
def test_agile_on_linear_sprint_intersects_gate_horizon(client):
|
|
user = provision_user_in_tenant(tenant_role="admin")
|
|
token = _login(client, user)
|
|
|
|
created = _create_initiative(
|
|
client,
|
|
token,
|
|
title="Sprint Horizon",
|
|
archetype_key="initiative.linear_project",
|
|
)
|
|
initiative_id = created.json()["id"]
|
|
g1_id, _ = _gate_ids(client, token, initiative_id)
|
|
_remove_starter_action(client, token, initiative_id)
|
|
|
|
cycle = client.post(
|
|
f"/api/initiatives/{initiative_id}/work-cycles",
|
|
json={"title": "Montage-Woche", "status": "active"},
|
|
headers=_auth(token),
|
|
)
|
|
assert cycle.status_code == 201
|
|
cycle_id = cycle.json()["id"]
|
|
|
|
outside_sprint = client.post(
|
|
f"/api/initiatives/{initiative_id}/actions",
|
|
json={
|
|
"title": "Nur Gate",
|
|
"status": "open",
|
|
"roadmap_item_id": g1_id,
|
|
},
|
|
headers=_auth(token),
|
|
)
|
|
in_sprint = client.post(
|
|
f"/api/initiatives/{initiative_id}/actions",
|
|
json={
|
|
"title": "Im Sprint",
|
|
"status": "ready",
|
|
"roadmap_item_id": g1_id,
|
|
"work_cycle_id": cycle_id,
|
|
},
|
|
headers=_auth(token),
|
|
)
|
|
assert outside_sprint.status_code == 201 and in_sprint.status_code == 201
|
|
|
|
snap = client.get(
|
|
f"/api/initiatives/{initiative_id}/steering-snapshot",
|
|
headers=_auth(token),
|
|
)
|
|
assert snap.status_code == 200
|
|
assert snap.json().get("active_work_cycle")
|
|
next_actions = snap.json().get("next_actions") or []
|
|
assert next_actions
|
|
assert next_actions[0]["action_id"] == in_sprint.json()["id"]
|
|
assert next_actions[0]["reason_code"] == "work_cycle_ready"
|
|
|
|
|
|
def test_operating_context_active_composition_modifier_with_sprint(client):
|
|
user = provision_user_in_tenant(tenant_role="member")
|
|
token = _login(client, user)
|
|
|
|
created = _create_initiative(
|
|
client,
|
|
token,
|
|
title="Composition Flag",
|
|
archetype_key="initiative.linear_project",
|
|
)
|
|
initiative_id = created.json()["id"]
|
|
|
|
ctx_before = client.get(
|
|
f"/api/initiatives/{initiative_id}/operating-context",
|
|
headers=_auth(token),
|
|
)
|
|
assert ctx_before.json().get("active_composition_modifier") is None
|
|
|
|
client.post(
|
|
f"/api/initiatives/{initiative_id}/work-cycles",
|
|
json={"title": "S1", "status": "active"},
|
|
headers=_auth(token),
|
|
)
|
|
|
|
ctx_after = client.get(
|
|
f"/api/initiatives/{initiative_id}/operating-context",
|
|
headers=_auth(token),
|
|
)
|
|
assert ctx_after.json().get("active_composition_modifier") == "agile_iteration"
|