All checks were successful
Deploy Development / deploy (push) Successful in 44s
Test Suite / pytest-backend (push) Successful in 1m49s
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 19s
Test Suite / playwright-smoke (push) Successful in 12s
Einzel-PATCH auf 0 kollidierte mit dem ersten Item; entspricht dem Batch-Reorder aus AP1.12c. Co-authored-by: Cursor <cursoragent@cursor.com>
310 lines
10 KiB
Python
310 lines
10 KiB
Python
"""AP0.8 — Operating Model Extension I tests.
|
|
|
|
Ausführung: im Backend-Container auf dem Pi nach Deploy (develop/main),
|
|
siehe .gitea/workflows/test.yml und docs/DEPLOYMENT.md.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from tests.factories import provision_user_in_tenant
|
|
from tests.test_initiatives_actions import (
|
|
_auth,
|
|
_create_action,
|
|
_create_initiative,
|
|
_login,
|
|
)
|
|
|
|
|
|
def _create_blocker(client, token, initiative_id, **kwargs):
|
|
body = {"title": "Blocker Test", **kwargs}
|
|
return client.post(
|
|
f"/api/initiatives/{initiative_id}/blockers",
|
|
json=body,
|
|
headers=_auth(token),
|
|
)
|
|
|
|
|
|
def _create_backlog(client, token, initiative_id, **kwargs):
|
|
body = {"title": "Backlog Test", **kwargs}
|
|
return client.post(
|
|
f"/api/initiatives/{initiative_id}/backlog",
|
|
json=body,
|
|
headers=_auth(token),
|
|
)
|
|
|
|
|
|
def _create_milestone(client, token, initiative_id, **kwargs):
|
|
body = {"title": "Milestone Test", **kwargs}
|
|
return client.post(
|
|
f"/api/initiatives/{initiative_id}/milestones",
|
|
json=body,
|
|
headers=_auth(token),
|
|
)
|
|
|
|
|
|
def test_attention_blocked_action(client):
|
|
user = provision_user_in_tenant(tenant_role="member")
|
|
token = _login(client, user)
|
|
initiative_id = _create_initiative(client, token).json()["id"]
|
|
action = _create_action(
|
|
client, token, initiative_id, assigned_actor_ids=[user["actor_id"]]
|
|
).json()
|
|
client.patch(
|
|
f"/api/actions/{action['id']}",
|
|
json={"status": "blocked"},
|
|
headers=_auth(token),
|
|
)
|
|
|
|
res = client.get("/api/workspace/attention", headers=_auth(token))
|
|
assert res.status_code == 200
|
|
items = res.json()
|
|
assert any(i["kind"] == "blocked_action" for i in items)
|
|
blocked = next(i for i in items if i["kind"] == "blocked_action")
|
|
assert blocked["reason_code"] == "action_blocked"
|
|
assert blocked["data_source"] == "actions"
|
|
assert blocked["scope_type"] == "action"
|
|
|
|
|
|
def test_attention_unassigned_action(client):
|
|
user = provision_user_in_tenant(tenant_role="member")
|
|
token = _login(client, user)
|
|
initiative_id = _create_initiative(client, token).json()["id"]
|
|
_create_action(client, token, initiative_id, title="Unassigned")
|
|
|
|
res = client.get("/api/workspace/attention", headers=_auth(token))
|
|
assert res.status_code == 200
|
|
assert any(i["kind"] == "unassigned_action" for i in res.json())
|
|
|
|
|
|
def test_attention_initiative_without_next_action(client):
|
|
user = provision_user_in_tenant(tenant_role="member")
|
|
token = _login(client, user)
|
|
_create_initiative(client, token, title="Empty Initiative")
|
|
|
|
res = client.get("/api/workspace/attention", headers=_auth(token))
|
|
assert res.status_code == 200
|
|
assert any(i["kind"] == "initiative_without_next_action" for i in res.json())
|
|
|
|
|
|
def test_attention_open_blocker(client):
|
|
user = provision_user_in_tenant(tenant_role="member")
|
|
token = _login(client, user)
|
|
initiative_id = _create_initiative(client, token).json()["id"]
|
|
_create_blocker(client, token, initiative_id, title="Dependency missing")
|
|
|
|
res = client.get("/api/workspace/attention", headers=_auth(token))
|
|
assert res.status_code == 200
|
|
assert any(i["kind"] == "open_blocker" for i in res.json())
|
|
|
|
|
|
def test_attention_milestone_at_risk(client):
|
|
user = provision_user_in_tenant(tenant_role="member")
|
|
token = _login(client, user)
|
|
initiative_id = _create_initiative(client, token).json()["id"]
|
|
ms = _create_milestone(client, token, initiative_id).json()
|
|
client.patch(
|
|
f"/api/milestones/{ms['id']}",
|
|
json={"status": "at_risk"},
|
|
headers=_auth(token),
|
|
)
|
|
|
|
res = client.get("/api/workspace/attention", headers=_auth(token))
|
|
assert any(i["kind"] == "milestone_at_risk" for i in res.json())
|
|
|
|
|
|
def test_attention_severity_sorting(client):
|
|
user = provision_user_in_tenant(tenant_role="member")
|
|
token = _login(client, user)
|
|
initiative_id = _create_initiative(client, token).json()["id"]
|
|
action = _create_action(client, token, initiative_id).json()
|
|
client.patch(
|
|
f"/api/actions/{action['id']}",
|
|
json={"status": "blocked"},
|
|
headers=_auth(token),
|
|
)
|
|
_create_blocker(client, token, initiative_id)
|
|
|
|
items = client.get("/api/workspace/attention", headers=_auth(token)).json()
|
|
severities = [i["severity"] for i in items]
|
|
order = {"critical": 0, "warning": 1, "info": 2}
|
|
assert severities == sorted(severities, key=lambda s: order.get(s, 99))
|
|
|
|
|
|
def test_next_action_convert_backlog(client):
|
|
user = provision_user_in_tenant(tenant_role="member")
|
|
token = _login(client, user)
|
|
initiative_id = _create_initiative(client, token).json()["id"]
|
|
item = _create_backlog(
|
|
client, token, initiative_id, status="accepted", title="Convert me"
|
|
).json()
|
|
|
|
res = client.get("/api/workspace/next-actions?limit=10", headers=_auth(token))
|
|
assert res.status_code == 200
|
|
assert any(
|
|
c["kind"] == "convert_backlog" and c["backlog_item_id"] == item["id"]
|
|
for c in res.json()
|
|
)
|
|
|
|
|
|
def test_next_action_limit(client):
|
|
user = provision_user_in_tenant(tenant_role="member")
|
|
token = _login(client, user)
|
|
initiative_id = _create_initiative(client, token).json()["id"]
|
|
for i in range(5):
|
|
_create_backlog(
|
|
client, token, initiative_id, status="accepted", title=f"Item {i}"
|
|
)
|
|
|
|
res = client.get("/api/workspace/next-actions?limit=2", headers=_auth(token))
|
|
assert len(res.json()) <= 2
|
|
|
|
|
|
def test_blocker_crud(client):
|
|
user = provision_user_in_tenant(tenant_role="member")
|
|
token = _login(client, user)
|
|
initiative_id = _create_initiative(client, token).json()["id"]
|
|
action = _create_action(client, token, initiative_id).json()
|
|
|
|
created = _create_blocker(
|
|
client,
|
|
token,
|
|
initiative_id,
|
|
title="API Block",
|
|
action_id=action["id"],
|
|
)
|
|
assert created.status_code == 201
|
|
blocker = created.json()
|
|
assert blocker["initiative_id"] == initiative_id
|
|
|
|
listed = client.get(
|
|
f"/api/initiatives/{initiative_id}/blockers", headers=_auth(token)
|
|
)
|
|
assert listed.status_code == 200
|
|
assert any(b["id"] == blocker["id"] for b in listed.json())
|
|
|
|
updated = client.patch(
|
|
f"/api/blockers/{blocker['id']}",
|
|
json={"status": "resolved"},
|
|
headers=_auth(token),
|
|
)
|
|
assert updated.status_code == 200
|
|
assert updated.json()["status"] == "resolved"
|
|
|
|
deleted = client.delete(f"/api/blockers/{blocker['id']}", headers=_auth(token))
|
|
assert deleted.status_code == 204
|
|
|
|
|
|
def test_blocker_cross_tenant(client):
|
|
user_a = provision_user_in_tenant()
|
|
user_b = provision_user_in_tenant()
|
|
token_a = _login(client, user_a)
|
|
token_b = _login(client, user_b)
|
|
initiative_id = _create_initiative(client, token_a).json()["id"]
|
|
blocker = _create_blocker(client, token_a, initiative_id).json()
|
|
|
|
assert (
|
|
client.get(f"/api/blockers/{blocker['id']}", headers=_auth(token_b)).status_code
|
|
== 404
|
|
)
|
|
|
|
|
|
def test_backlog_convert_to_action(client):
|
|
user = provision_user_in_tenant(tenant_role="member")
|
|
token = _login(client, user)
|
|
initiative_id = _create_initiative(client, token).json()["id"]
|
|
item = _create_backlog(
|
|
client, token, initiative_id, status="accepted", title="Feature X"
|
|
).json()
|
|
|
|
converted = client.post(
|
|
f"/api/backlog/{item['id']}/convert-to-action",
|
|
json={},
|
|
headers=_auth(token),
|
|
)
|
|
assert converted.status_code == 201
|
|
body = converted.json()
|
|
assert body["backlog_item"]["status"] == "converted"
|
|
assert body["action"]["title"] == "Feature X"
|
|
assert body["backlog_item"]["converted_action_id"] == body["action"]["id"]
|
|
|
|
|
|
def test_backlog_crud(client):
|
|
user = provision_user_in_tenant(tenant_role="member")
|
|
token = _login(client, user)
|
|
initiative_id = _create_initiative(client, token).json()["id"]
|
|
created = _create_backlog(client, token, initiative_id, status="triaged")
|
|
assert created.status_code == 201
|
|
item = created.json()
|
|
|
|
patched = client.patch(
|
|
f"/api/backlog/{item['id']}",
|
|
json={"status": "accepted"},
|
|
headers=_auth(token),
|
|
)
|
|
assert patched.status_code == 200
|
|
|
|
deleted = client.delete(f"/api/backlog/{item['id']}", headers=_auth(token))
|
|
assert deleted.status_code == 204
|
|
|
|
|
|
def test_backlog_sort_order(client):
|
|
user = provision_user_in_tenant(tenant_role="member")
|
|
token = _login(client, user)
|
|
initiative_id = _create_initiative(client, token).json()["id"]
|
|
first = _create_backlog(client, token, initiative_id, title="First").json()
|
|
second = _create_backlog(client, token, initiative_id, title="Second").json()
|
|
assert first["sort_order"] < second["sort_order"]
|
|
|
|
# Reorder wie im Frontend: beide Geschwister sort_order neu setzen (kein Tie)
|
|
bumped = client.patch(
|
|
f"/api/backlog/{first['id']}",
|
|
json={"sort_order": 10},
|
|
headers=_auth(token),
|
|
)
|
|
assert bumped.status_code == 200
|
|
patched = client.patch(
|
|
f"/api/backlog/{second['id']}",
|
|
json={"sort_order": 0},
|
|
headers=_auth(token),
|
|
)
|
|
assert patched.status_code == 200
|
|
assert patched.json()["sort_order"] == 0
|
|
|
|
listed = client.get(
|
|
f"/api/initiatives/{initiative_id}/backlog",
|
|
headers=_auth(token),
|
|
).json()
|
|
assert [row["title"] for row in listed[:2]] == ["Second", "First"]
|
|
|
|
|
|
def test_milestone_crud(client):
|
|
user = provision_user_in_tenant(tenant_role="member")
|
|
token = _login(client, user)
|
|
initiative_id = _create_initiative(client, token).json()["id"]
|
|
created = _create_milestone(
|
|
client, token, initiative_id, status="active", target_date="2026-12-31"
|
|
)
|
|
assert created.status_code == 201
|
|
ms = created.json()
|
|
assert ms["target_date"] == "2026-12-31"
|
|
|
|
deleted = client.delete(f"/api/milestones/{ms['id']}", headers=_auth(token))
|
|
assert deleted.status_code == 204
|
|
|
|
|
|
def test_member_has_ap08_capabilities(client):
|
|
member = provision_user_in_tenant(tenant_role="member")
|
|
token = _login(client, member)
|
|
ctx = client.get("/api/me/context", headers=_auth(token)).json()
|
|
caps = ctx["capabilities"]
|
|
for key in (
|
|
"kairo.attention.read",
|
|
"kairo.blocker.read",
|
|
"kairo.blocker.manage",
|
|
"kairo.backlog.read",
|
|
"kairo.backlog.manage",
|
|
"kairo.milestone.read",
|
|
"kairo.milestone.manage",
|
|
):
|
|
assert key in caps
|