Files
openclaw-mission-control/backend/tests/test_approvals_pending_conflicts.py

183 lines
6.7 KiB
Python

from __future__ import annotations
from uuid import UUID, uuid4
import pytest
from fastapi import HTTPException
from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine
from sqlmodel import SQLModel
from sqlmodel.ext.asyncio.session import AsyncSession
from app.api import approvals as approvals_api
from app.models.boards import Board
from app.models.organizations import Organization
from app.models.tasks import Task
from app.schemas.approvals import ApprovalCreate, ApprovalUpdate
async def _make_engine() -> AsyncEngine:
engine = create_async_engine("sqlite+aiosqlite:///:memory:")
async with engine.connect() as conn, conn.begin():
await conn.run_sync(SQLModel.metadata.create_all)
return engine
async def _make_session(engine: AsyncEngine) -> AsyncSession:
return AsyncSession(engine, expire_on_commit=False)
async def _seed_board_with_tasks(
session: AsyncSession,
*,
task_count: int = 2,
) -> tuple[Board, list[UUID]]:
org_id = uuid4()
board = Board(id=uuid4(), organization_id=org_id, name="b", slug="b")
task_ids = [uuid4() for _ in range(task_count)]
session.add(Organization(id=org_id, name=f"org-{org_id}"))
session.add(board)
for task_id in task_ids:
session.add(Task(id=task_id, board_id=board.id, title=f"task-{task_id}"))
await session.commit()
return board, task_ids
@pytest.mark.asyncio
async def test_create_approval_rejects_duplicate_pending_for_same_task() -> None:
engine = await _make_engine()
try:
async with await _make_session(engine) as session:
board, task_ids = await _seed_board_with_tasks(session, task_count=1)
task_id = task_ids[0]
created = await approvals_api.create_approval(
payload=ApprovalCreate(
action_type="task.execute",
task_id=task_id,
payload={"reason": "Initial execution needs confirmation."},
confidence=80,
status="pending",
),
board=board,
session=session,
)
assert created.task_titles == [f"task-{task_id}"]
with pytest.raises(HTTPException) as exc:
await approvals_api.create_approval(
payload=ApprovalCreate(
action_type="task.retry",
task_id=task_id,
payload={"reason": "Retry should still be gated."},
confidence=77,
status="pending",
),
board=board,
session=session,
)
assert exc.value.status_code == 409
detail = exc.value.detail
assert isinstance(detail, dict)
assert detail["message"] == "Each task can have only one pending approval."
assert len(detail["conflicts"]) == 1
assert detail["conflicts"][0]["task_id"] == str(task_id)
finally:
await engine.dispose()
@pytest.mark.asyncio
async def test_create_approval_rejects_pending_conflict_from_linked_task_ids() -> None:
engine = await _make_engine()
try:
async with await _make_session(engine) as session:
board, task_ids = await _seed_board_with_tasks(session, task_count=2)
task_a, task_b = task_ids
created = await approvals_api.create_approval(
payload=ApprovalCreate(
action_type="task.batch_execute",
task_ids=[task_a, task_b],
payload={"reason": "Batch operation requires sign-off."},
confidence=85,
status="pending",
),
board=board,
session=session,
)
assert created.task_titles == [f"task-{task_a}", f"task-{task_b}"]
with pytest.raises(HTTPException) as exc:
await approvals_api.create_approval(
payload=ApprovalCreate(
action_type="task.execute",
task_id=task_b,
payload={"reason": "Single task overlaps with pending batch."},
confidence=70,
status="pending",
),
board=board,
session=session,
)
assert exc.value.status_code == 409
detail = exc.value.detail
assert isinstance(detail, dict)
assert detail["message"] == "Each task can have only one pending approval."
assert len(detail["conflicts"]) == 1
assert detail["conflicts"][0]["task_id"] == str(task_b)
finally:
await engine.dispose()
@pytest.mark.asyncio
async def test_update_approval_rejects_reopening_to_pending_with_existing_pending() -> None:
engine = await _make_engine()
try:
async with await _make_session(engine) as session:
board, task_ids = await _seed_board_with_tasks(session, task_count=1)
task_id = task_ids[0]
pending = await approvals_api.create_approval(
payload=ApprovalCreate(
action_type="task.execute",
task_id=task_id,
payload={"reason": "Primary pending approval is active."},
confidence=83,
status="pending",
),
board=board,
session=session,
)
resolved = await approvals_api.create_approval(
payload=ApprovalCreate(
action_type="task.review",
task_id=task_id,
payload={"reason": "Review decision completed earlier."},
confidence=90,
status="approved",
),
board=board,
session=session,
)
with pytest.raises(HTTPException) as exc:
await approvals_api.update_approval(
approval_id=resolved.id, # type: ignore[arg-type]
payload=ApprovalUpdate(status="pending"),
board=board,
session=session,
)
assert exc.value.status_code == 409
detail = exc.value.detail
assert isinstance(detail, dict)
assert detail["message"] == "Each task can have only one pending approval."
assert detail["conflicts"] == [
{
"task_id": str(task_id),
"approval_id": str(pending.id),
},
]
finally:
await engine.dispose()