feat(AP1.7b): Op-API fuer Backlog-Commit, Recurring-Status und Sprint-Zuweisung
Some checks failed
Deploy Development / deploy (push) Successful in 52s
Test Suite / pytest-backend (push) Failing after 4m37s
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 2s
Test Suite / compose-smoke (push) Has been skipped
Some checks failed
Deploy Development / deploy (push) Successful in 52s
Test Suite / pytest-backend (push) Failing after 4m37s
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 2s
Test Suite / compose-smoke (push) Has been skipped
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
252b3f05f2
commit
69c516dba4
|
|
@ -14,6 +14,7 @@ from services import backlog as backlog_service
|
|||
from services import blockers as blocker_service
|
||||
from services import decisions as decision_service
|
||||
from services import evidence as evidence_service
|
||||
from services import recurring as recurring_service
|
||||
from services.audit import log_audit
|
||||
from tenant_context import TenantContext
|
||||
|
||||
|
|
@ -74,6 +75,21 @@ class BacklogProposalBody(BaseModel):
|
|||
roadmap_item_id: Optional[str] = None
|
||||
|
||||
|
||||
class BacklogConvertBody(BaseModel):
|
||||
work_cycle_id: Optional[str] = None
|
||||
assign_active_sprint: bool = True
|
||||
assigned_actor_ids: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class RecurringStatusPatch(BaseModel):
|
||||
status: Literal["active", "paused", "ended"]
|
||||
|
||||
|
||||
class ActionWorkCyclePatch(BaseModel):
|
||||
work_cycle_id: Optional[str] = None
|
||||
clear_work_cycle: bool = False
|
||||
|
||||
|
||||
@router.get("/me")
|
||||
def operational_me(ctx: TenantContext = Depends(require_operational_capability("kairo.action.read"))):
|
||||
return _ok(
|
||||
|
|
@ -309,3 +325,105 @@ def operational_backlog_proposal(
|
|||
raise HTTPException(status_code=404, detail=detail) from exc
|
||||
raise HTTPException(status_code=400, detail=detail) from exc
|
||||
return _ok(ctx, item)
|
||||
|
||||
|
||||
@router.post("/backlog/{backlog_item_id}/convert-to-action", status_code=201)
|
||||
def operational_convert_backlog(
|
||||
backlog_item_id: str,
|
||||
body: BacklogConvertBody,
|
||||
ctx: TenantContext = Depends(require_operational_capability("kairo.backlog.manage")),
|
||||
):
|
||||
assigned = body.assigned_actor_ids
|
||||
if not assigned and ctx.actor_id:
|
||||
assigned = [ctx.actor_id]
|
||||
try:
|
||||
result = backlog_service.convert_backlog_to_action(
|
||||
tenant_id=ctx.tenant_id,
|
||||
backlog_item_id=backlog_item_id,
|
||||
user_id=ctx.user_id,
|
||||
assigned_actor_ids=assigned,
|
||||
work_cycle_id=body.work_cycle_id,
|
||||
assign_active_sprint=body.assign_active_sprint,
|
||||
)
|
||||
except ValueError as exc:
|
||||
detail = str(exc)
|
||||
if detail == "Backlog-Item nicht gefunden":
|
||||
raise HTTPException(status_code=404, detail=detail) from exc
|
||||
raise HTTPException(status_code=400, detail=detail) from exc
|
||||
|
||||
log_audit(
|
||||
"operational.backlog.convert",
|
||||
user_id=ctx.user_id or None,
|
||||
tenant_id=ctx.tenant_id,
|
||||
details={
|
||||
"backlog_item_id": backlog_item_id,
|
||||
"action_id": result.get("action", {}).get("id"),
|
||||
"work_cycle_id": body.work_cycle_id,
|
||||
"actor_id": ctx.actor_id,
|
||||
},
|
||||
)
|
||||
return _ok(ctx, result)
|
||||
|
||||
|
||||
@router.patch("/recurring/{recurring_id}")
|
||||
def operational_patch_recurring(
|
||||
recurring_id: str,
|
||||
body: RecurringStatusPatch,
|
||||
ctx: TenantContext = Depends(require_operational_capability("kairo.recurring.manage")),
|
||||
):
|
||||
try:
|
||||
item = recurring_service.update_recurring_element(
|
||||
tenant_id=ctx.tenant_id,
|
||||
recurring_id=recurring_id,
|
||||
user_id=ctx.user_id,
|
||||
status=body.status,
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
if not item:
|
||||
raise HTTPException(status_code=404, detail="Recurring-Element nicht gefunden")
|
||||
|
||||
log_audit(
|
||||
"operational.recurring.status",
|
||||
user_id=ctx.user_id or None,
|
||||
tenant_id=ctx.tenant_id,
|
||||
details={
|
||||
"recurring_id": recurring_id,
|
||||
"status": body.status,
|
||||
"actor_id": ctx.actor_id,
|
||||
},
|
||||
)
|
||||
return _ok(ctx, item)
|
||||
|
||||
|
||||
@router.patch("/actions/{action_id}/work-cycle")
|
||||
def operational_patch_action_work_cycle(
|
||||
action_id: str,
|
||||
body: ActionWorkCyclePatch,
|
||||
ctx: TenantContext = Depends(require_operational_capability("kairo.action.manage")),
|
||||
):
|
||||
try:
|
||||
item = action_service.update_action(
|
||||
tenant_id=ctx.tenant_id,
|
||||
action_id=action_id,
|
||||
user_id=ctx.user_id or None,
|
||||
work_cycle_id=body.work_cycle_id,
|
||||
clear_work_cycle=body.clear_work_cycle,
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
if not item:
|
||||
raise HTTPException(status_code=404, detail="Arbeitspaket nicht gefunden")
|
||||
|
||||
log_audit(
|
||||
"operational.action.work_cycle",
|
||||
user_id=ctx.user_id or None,
|
||||
tenant_id=ctx.tenant_id,
|
||||
details={
|
||||
"action_id": action_id,
|
||||
"work_cycle_id": body.work_cycle_id,
|
||||
"clear_work_cycle": body.clear_work_cycle,
|
||||
"actor_id": ctx.actor_id,
|
||||
},
|
||||
)
|
||||
return _ok(ctx, item)
|
||||
|
|
|
|||
136
backend/tests/test_ap17b_operational_api.py
Normal file
136
backend/tests/test_ap17b_operational_api.py
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
"""AP1.7b — Operational API Pflege-Parität (Backlog-Commit, Recurring, Sprint-Zuweisung)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from services import actors as actor_service
|
||||
from services.actor_service_tokens import create_service_token
|
||||
from tests.factories import provision_user_in_tenant
|
||||
from tests.test_initiatives_actions import _auth, _create_initiative, _login
|
||||
|
||||
|
||||
def _agent_token(admin, *, capabilities=None):
|
||||
agent = actor_service.create_actor(
|
||||
tenant_id=admin["tenant_id"],
|
||||
actor_type="agent",
|
||||
name="Ops Agent",
|
||||
)
|
||||
caps = capabilities or [
|
||||
"kairo.action.read",
|
||||
"kairo.action.manage",
|
||||
"kairo.backlog.manage",
|
||||
"kairo.recurring.manage",
|
||||
"kairo.initiative.read",
|
||||
]
|
||||
created = create_service_token(
|
||||
tenant_id=admin["tenant_id"],
|
||||
actor_id=agent["id"],
|
||||
label="AP17b",
|
||||
created_by_user_id=admin["id"],
|
||||
capabilities=caps,
|
||||
)
|
||||
return created["token"], agent["id"]
|
||||
|
||||
|
||||
def test_operational_convert_backlog_to_sprint(client):
|
||||
admin = provision_user_in_tenant(tenant_role="admin")
|
||||
user_token = _login(client, admin)
|
||||
actor_token, _ = _agent_token(admin)
|
||||
|
||||
created = _create_initiative(
|
||||
client,
|
||||
user_token,
|
||||
title="Product Ops",
|
||||
archetype_key="initiative.product",
|
||||
)
|
||||
initiative_id = created.json()["id"]
|
||||
|
||||
cycle = client.post(
|
||||
f"/api/initiatives/{initiative_id}/work-cycles",
|
||||
json={"title": "Sprint Ops", "status": "active"},
|
||||
headers=_auth(user_token),
|
||||
)
|
||||
assert cycle.status_code == 201
|
||||
cycle_id = cycle.json()["id"]
|
||||
|
||||
backlog = client.post(
|
||||
f"/api/initiatives/{initiative_id}/backlog",
|
||||
json={"title": "Via Agent", "status": "accepted"},
|
||||
headers=_auth(user_token),
|
||||
)
|
||||
backlog_id = backlog.json()["id"]
|
||||
|
||||
converted = client.post(
|
||||
f"/api/operational/backlog/{backlog_id}/convert-to-action",
|
||||
json={"work_cycle_id": cycle_id, "assign_active_sprint": False},
|
||||
headers={"X-Actor-Token": actor_token},
|
||||
)
|
||||
assert converted.status_code == 201
|
||||
body = converted.json()
|
||||
assert body["ok"] is True
|
||||
assert body["data"]["action"]["work_cycle_id"] == cycle_id
|
||||
|
||||
|
||||
def test_operational_patch_recurring_status(client):
|
||||
admin = provision_user_in_tenant(tenant_role="admin")
|
||||
user_token = _login(client, admin)
|
||||
actor_token, _ = _agent_token(admin)
|
||||
|
||||
created = _create_initiative(
|
||||
client,
|
||||
user_token,
|
||||
title="Spagat Ops",
|
||||
archetype_key="initiative.maturity_journey",
|
||||
apply_starter_kit=True,
|
||||
)
|
||||
initiative_id = created.json()["id"]
|
||||
|
||||
recurring = client.get(
|
||||
f"/api/initiatives/{initiative_id}/recurring",
|
||||
headers=_auth(user_token),
|
||||
).json()
|
||||
assert recurring
|
||||
recurring_id = recurring[0]["id"]
|
||||
|
||||
patched = client.patch(
|
||||
f"/api/operational/recurring/{recurring_id}",
|
||||
json={"status": "paused"},
|
||||
headers={"X-Actor-Token": actor_token},
|
||||
)
|
||||
assert patched.status_code == 200
|
||||
assert patched.json()["data"]["status"] == "paused"
|
||||
|
||||
|
||||
def test_operational_assign_action_work_cycle(client):
|
||||
admin = provision_user_in_tenant(tenant_role="admin")
|
||||
user_token = _login(client, admin)
|
||||
actor_token, _ = _agent_token(admin)
|
||||
|
||||
created = _create_initiative(
|
||||
client,
|
||||
user_token,
|
||||
title="Sprint Assign",
|
||||
archetype_key="initiative.product",
|
||||
)
|
||||
initiative_id = created.json()["id"]
|
||||
|
||||
cycle = client.post(
|
||||
f"/api/initiatives/{initiative_id}/work-cycles",
|
||||
json={"title": "Sprint B", "status": "planned"},
|
||||
headers=_auth(user_token),
|
||||
)
|
||||
cycle_id = cycle.json()["id"]
|
||||
|
||||
action = client.post(
|
||||
f"/api/initiatives/{initiative_id}/actions",
|
||||
json={"title": "Loose AP", "status": "open"},
|
||||
headers=_auth(user_token),
|
||||
)
|
||||
action_id = action.json()["id"]
|
||||
|
||||
assigned = client.patch(
|
||||
f"/api/operational/actions/{action_id}/work-cycle",
|
||||
json={"work_cycle_id": cycle_id},
|
||||
headers={"X-Actor-Token": actor_token},
|
||||
)
|
||||
assert assigned.status_code == 200
|
||||
assert assigned.json()["data"]["work_cycle_id"] == cycle_id
|
||||
|
|
@ -235,7 +235,7 @@ Details: `Kairo_Status_Review_and_Next_Steps_v0.1.md` §2.1
|
|||
| AP1.9d | Work-Composition + Drill-down ◐→✓ (2026-07-27) |
|
||||
| AP2.2c–e | Referenz-Archetypen End-to-End ◐→✓ |
|
||||
| AP2.1 | MVP-Abnahfe ✗→✓ |
|
||||
| AP1.7b | Op-API Parität |
|
||||
| AP1.7b | Op-API Parität ◐→✓ (2026-07-27) |
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ Phase 3 AP2.2b A2 Linear End-to-End ✓
|
|||
AP2.2c A1 Reifegrad + AP2.0e ✓
|
||||
AP2.2d B2b Product + B3 Sprint ✓
|
||||
AP2.2e B2a Programm (optional vor AP2.1)
|
||||
Phase 4 AP1.7b Operational API — Pflege-Parität
|
||||
Phase 4 AP1.7b Operational API — Pflege-Parität ✓
|
||||
Phase 5 AP2.1 Validation Report v0.3 (alle Stufe-A-Szenarien)
|
||||
Phase 6 AP2.2f Stufe B: A3, B1, D1
|
||||
Phase 7 Schicht 4: Gitea-Webhook, MCP (nach AP2.1 Go)
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user