From 5169b12795fa1fb54e382fa7b5316d0437a561da Mon Sep 17 00:00:00 2001 From: Agent Zero Date: Fri, 18 Sep 2026 11:08:49 +0200 Subject: [PATCH] =?UTF-8?q?fix(workflows):=20F12=20(Astra=20P1)=20?= =?UTF-8?q?=E2=80=94=20approve/reject=20an=20zentralen=20Approval-Vertrag?= =?UTF-8?q?=20anpassen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Vorher: Beide Routen behandelten die Rueckgabe von create_approval_request als Dictionary (approval["id"] -> TypeError: ApprovalRequest object is not subscriptable, Astra-Repro), riefen resolve_approval_request mit nicht existierendem decided_by statt approver_id und ohne tenant_id auf — und erzeugten bei JEDEM Aufruf eine NEUE Anfrage, die sie sofort selbst genehmigten, statt die wartende Engine-Anfrage aufzuloesen. Fix (beide Routen, approve + reject): - Suchen die BESTEHENDE pending ApprovalRequest der Engine (entity_type=workflow_instance, entity_id, status=pending, neueste zuerst) und loesen genau diese auf — keine Selbst-Genehmigung mehr - Korrekte F11-Signatur: (db, tenant_id, request_id, decision=, approver_id=, comment=, is_system_admin=) + ApprovalDecisionError- Behandlung (403/409/410) Keine wartende Anfrage -> 409 no_pending_approval (kla rer Zustand statt stiller Neubau) - advance_instance/cancel_instance laufen wie gehabt NACH erfolgreicher Aufloesung Abnahme (Astra): Beide URLs funktionieren; Zustandswechsel, Audit und Freigabe stimmen; Wiederholung erzeugt keinen zweiten Fortschritt — erfuellt (resolve wirft 409 not_pending bei Zweitentscheid). Tests: test_phase_g_workflows + test_s1_security_guards 60/60. ruff clean. --- app/routes/workflows.py | 130 +++++++++++++++++++++++++++++----------- 1 file changed, 96 insertions(+), 34 deletions(-) diff --git a/app/routes/workflows.py b/app/routes/workflows.py index 51d0e63..cfabf52 100644 --- a/app/routes/workflows.py +++ b/app/routes/workflows.py @@ -464,7 +464,14 @@ async def approve_workflow_step( db: AsyncSession = Depends(get_db), current_user: dict = Depends(require_permission("workflows:write")), ): - """Approve the current approval step of a workflow instance.""" + """Approve the current approval step of a workflow instance. + + F12 (Astra P1): resolves the EXISTING pending approval request that + the workflow engine created when the decision guard paused the flow — + instead of creating a NEW request and self-approving it (old behaviour: + TypeError on approval["id"] — create returns an ORM object, and the + call used a nonexistent ``decided_by`` kwarg). + """ tenant_id = uuid.UUID(current_user["tenant_id"]) user_id = uuid.UUID(current_user["user_id"]) @@ -476,7 +483,11 @@ async def approve_workflow_step( from sqlalchemy import select - from app.core.approval import create_approval_request, resolve_approval_request + from app.core.approval import ( + ApprovalDecisionError, + ApprovalRequest, + resolve_approval_request, + ) from app.models.workflow import WorkflowInstance result = await db.execute( @@ -493,22 +504,44 @@ async def approve_workflow_step( detail={"detail": "Instance not found", "code": "not_found"}, ) - approval = await create_approval_request( - db=db, - tenant_id=tenant_id, - entity_type="workflow_instance", - entity_id=uuid.UUID(instance_id), - action="workflow_step_approval", - requested_by=user_id, - requested_by_type="user", - ) - await resolve_approval_request( - db=db, - request_id=approval["id"], - decision="approved", - decided_by=user_id, - comment=comment, + # F12: find the EXISTING pending approval the engine created for this + # instance — do not create a new, instantly self-approved one. + pending_result = await db.execute( + select(ApprovalRequest) + .where( + ApprovalRequest.tenant_id == tenant_id, + ApprovalRequest.entity_type == "workflow_instance", + ApprovalRequest.entity_id == instance.id, + ApprovalRequest.status == "pending", + ) + .order_by(ApprovalRequest.created_at.desc()) + .limit(1) ) + pending = pending_result.scalar_one_or_none() + if pending is None: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail={ + "detail": "No pending approval request for this workflow instance", + "code": "no_pending_approval", + }, + ) + + try: + await resolve_approval_request( + db, + tenant_id, + pending.id, + decision="approved", + approver_id=user_id, + comment=comment, + is_system_admin=bool(current_user.get("is_system_admin", False)), + ) + except ApprovalDecisionError as exc: + raise HTTPException( + status_code=exc.http_status, + detail={"detail": str(exc), "code": exc.code}, + ) from exc return await workflow_service.advance_instance( db, @@ -528,7 +561,11 @@ async def reject_workflow_step( db: AsyncSession = Depends(get_db), current_user: dict = Depends(require_permission("workflows:write")), ): - """Reject the current approval step of a workflow instance.""" + """Reject the current approval step of a workflow instance. + + F12 (Astra P1): resolves the EXISTING pending approval request that + the workflow engine created — same contract fix as approve. + """ tenant_id = uuid.UUID(current_user["tenant_id"]) user_id = uuid.UUID(current_user["user_id"]) @@ -540,7 +577,11 @@ async def reject_workflow_step( from sqlalchemy import select - from app.core.approval import create_approval_request, resolve_approval_request + from app.core.approval import ( + ApprovalDecisionError, + ApprovalRequest, + resolve_approval_request, + ) from app.models.workflow import WorkflowInstance result = await db.execute( @@ -557,22 +598,43 @@ async def reject_workflow_step( detail={"detail": "Instance not found", "code": "not_found"}, ) - approval = await create_approval_request( - db=db, - tenant_id=tenant_id, - entity_type="workflow_instance", - entity_id=uuid.UUID(instance_id), - action="workflow_step_approval", - requested_by=user_id, - requested_by_type="user", - ) - await resolve_approval_request( - db=db, - request_id=approval["id"], - decision="rejected", - decided_by=user_id, - comment=comment, + # F12: resolve the EXISTING pending approval for this instance. + pending_result = await db.execute( + select(ApprovalRequest) + .where( + ApprovalRequest.tenant_id == tenant_id, + ApprovalRequest.entity_type == "workflow_instance", + ApprovalRequest.entity_id == instance.id, + ApprovalRequest.status == "pending", + ) + .order_by(ApprovalRequest.created_at.desc()) + .limit(1) ) + pending = pending_result.scalar_one_or_none() + if pending is None: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail={ + "detail": "No pending approval request for this workflow instance", + "code": "no_pending_approval", + }, + ) + + try: + await resolve_approval_request( + db, + tenant_id, + pending.id, + decision="rejected", + approver_id=user_id, + comment=comment, + is_system_admin=bool(current_user.get("is_system_admin", False)), + ) + except ApprovalDecisionError as exc: + raise HTTPException( + status_code=exc.http_status, + detail={"detail": str(exc), "code": exc.code}, + ) from exc return await workflow_service.cancel_instance( db,