feat: approval decisions, tenant scoping, error paths with tests

- POST /api/approvals/{id}/decision (approve starts continuation flow
  with task facts; reject closes; 404/409/422 guards)
- Tenant scoping: list filters + cross-tenant reads 404
- Error mapping covered: LFX down 503, LFX drift 502, corrupt flow 422
- 34 tests green
This commit is contained in:
Jonas Miederer 2026-09-06 23:06:34 +00:00
parent 1034206870
commit 6c195bc2e7
4 changed files with 389 additions and 12 deletions

View file

@ -174,16 +174,19 @@ def get_run(session: Session, run_id: UUID) -> FlowRun:
return run
def list_runs(session: Session, flow_id: UUID) -> list[FlowRun]:
def list_runs(
session: Session, flow_id: UUID, tenant_id: str | None = None
) -> list[FlowRun]:
get_definition(session, flow_id)
return list(
session.exec(
select(FlowRun)
.join(FlowVersion, FlowVersion.id == FlowRun.version_id)
.where(FlowVersion.flow_id == flow_id)
.order_by(FlowRun.created_at.desc())
).all()
query = (
select(FlowRun)
.join(FlowVersion, FlowVersion.id == FlowRun.version_id)
.where(FlowVersion.flow_id == flow_id)
.order_by(FlowRun.created_at.desc())
)
if tenant_id is not None:
query = query.where(FlowRun.tenant_id == tenant_id)
return list(session.exec(query).all())
def create_approval_task(session: Session, data: ApprovalCreate) -> ApprovalTask:
@ -223,3 +226,33 @@ def list_approval_tasks(session: Session, tenant_id: str | None = None) -> list[
if tenant_id is not None:
query = query.where(ApprovalTask.tenant_id == tenant_id)
return list(session.exec(query).all())
def get_task(session: Session, task_id: UUID) -> ApprovalTask:
task = session.get(ApprovalTask, task_id)
if task is None:
raise NotFoundError(f"approval task {task_id} not found")
return task
def decide_task(
session: Session,
task: ApprovalTask,
*,
decision: str,
approver: str = "",
note: str = "",
) -> ApprovalTask:
"""Approve or reject an open task. Decided tasks are immutable (409)."""
if task.status != "open":
raise ConflictError(f"task {task.id} is already {task.status}")
if decision not in ("approved", "rejected"):
raise InvalidError(f"unknown decision {decision!r}")
task.status = decision
task.approver = approver
task.decision_note = note
task.decided_at = datetime.now(timezone.utc)
session.add(task)
session.commit()
session.refresh(task)
return task