# ruff: noqa: INP001 from __future__ import annotations from uuid import uuid4 import pytest from fastapi import HTTPException from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine from sqlmodel import SQLModel, col, select from sqlmodel.ext.asyncio.session import AsyncSession from app.api.deps import ActorContext from app.api.tasks import _apply_lead_task_update, _TaskUpdateInput from app.models.agents import Agent from app.models.boards import Board from app.models.organizations import Organization from app.models.task_dependencies import TaskDependency from app.models.tasks import Task 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) @pytest.mark.asyncio async def test_lead_update_rejects_assignment_change_when_task_blocked() -> None: engine = await _make_engine() try: async with await _make_session(engine) as session: org_id = uuid4() board_id = uuid4() lead_id = uuid4() worker_id = uuid4() dep_id = uuid4() task_id = uuid4() session.add(Organization(id=org_id, name="org")) session.add(Board(id=board_id, organization_id=org_id, name="b", slug="b")) session.add( Agent( id=lead_id, name="Lead", board_id=board_id, gateway_id=uuid4(), is_board_lead=True, openclaw_session_id="agent:lead:session", ), ) session.add( Agent( id=worker_id, name="Worker", board_id=board_id, gateway_id=uuid4(), is_board_lead=False, openclaw_session_id="agent:worker:session", ), ) session.add(Task(id=dep_id, board_id=board_id, title="dep", description=None)) session.add( Task( id=task_id, board_id=board_id, title="t", description=None, status="review", assigned_agent_id=None, ), ) session.add( TaskDependency( board_id=board_id, task_id=task_id, depends_on_task_id=dep_id, ), ) await session.commit() lead = (await session.exec(select(Agent).where(col(Agent.id) == lead_id))).first() task = (await session.exec(select(Task).where(col(Task.id) == task_id))).first() assert lead is not None assert task is not None update = _TaskUpdateInput( task=task, actor=ActorContext(actor_type="agent", agent=lead), board_id=board_id, previous_status=task.status, previous_assigned=task.assigned_agent_id, status_requested=False, updates={"assigned_agent_id": worker_id}, comment=None, depends_on_task_ids=None, tag_ids=None, custom_field_values={}, custom_field_values_set=False, ) with pytest.raises(HTTPException) as exc: await _apply_lead_task_update(session, update=update) assert exc.value.status_code == 409 detail = exc.value.detail assert isinstance(detail, dict) assert detail["code"] == "task_blocked_cannot_transition" assert detail["blocked_by_task_ids"] == [str(dep_id)] # DB unchanged reloaded = (await session.exec(select(Task).where(col(Task.id) == task_id))).first() assert reloaded is not None assert reloaded.status == "review" assert reloaded.assigned_agent_id is None finally: await engine.dispose() @pytest.mark.asyncio async def test_lead_update_rejects_status_change_when_task_blocked() -> None: engine = await _make_engine() try: async with await _make_session(engine) as session: org_id = uuid4() board_id = uuid4() lead_id = uuid4() dep_id = uuid4() task_id = uuid4() session.add(Organization(id=org_id, name="org")) session.add(Board(id=board_id, organization_id=org_id, name="b", slug="b")) session.add( Agent( id=lead_id, name="Lead", board_id=board_id, gateway_id=uuid4(), is_board_lead=True, openclaw_session_id="agent:lead:session", ), ) session.add(Task(id=dep_id, board_id=board_id, title="dep", description=None)) session.add( Task( id=task_id, board_id=board_id, title="t", description=None, status="review", ), ) session.add( TaskDependency( board_id=board_id, task_id=task_id, depends_on_task_id=dep_id, ), ) await session.commit() lead = (await session.exec(select(Agent).where(col(Agent.id) == lead_id))).first() task = (await session.exec(select(Task).where(col(Task.id) == task_id))).first() assert lead is not None assert task is not None update = _TaskUpdateInput( task=task, actor=ActorContext(actor_type="agent", agent=lead), board_id=board_id, previous_status=task.status, previous_assigned=task.assigned_agent_id, status_requested=True, updates={"status": "done"}, comment=None, depends_on_task_ids=None, tag_ids=None, custom_field_values={}, custom_field_values_set=False, ) with pytest.raises(HTTPException) as exc: await _apply_lead_task_update(session, update=update) assert exc.value.status_code == 409 detail = exc.value.detail assert isinstance(detail, dict) assert detail["code"] == "task_blocked_cannot_transition" assert detail["blocked_by_task_ids"] == [str(dep_id)] reloaded = (await session.exec(select(Task).where(col(Task.id) == task_id))).first() assert reloaded is not None assert reloaded.status == "review" finally: await engine.dispose()