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

203 lines
7.0 KiB
Python

# 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()