"""A dev/code subtask under a frontend cell task that is waiting on the UX/UI design must inherit the unresolved cross-cell dependency, so the developer is held until UX is done instead of coding ahead of the design. The frontend CELL task already waits on the UX/UI cell task (cross-cell sequencing). When the cell PM delegates a dev subtask under that cell task, the subtask must NOT become dispatchable while the UX dependency is unresolved, and must become dispatchable once the UX task reaches a terminal state. """ from __future__ import annotations from typing import TYPE_CHECKING from uuid import uuid4 import pytest import pytest_asyncio from roboco.db.tables import AgentTable, ProductTable, ProjectTable from roboco.models import AgentRole, AgentStatus, Team from roboco.models.base import Complexity, TaskNature, TaskStatus, TaskType from roboco.models.task import TaskCreateRequest from roboco.services.gateway.choreographer._impl import ( Choreographer, ChoreographerDeps, ) from roboco.services.task import TaskService if TYPE_CHECKING: from collections.abc import AsyncIterator from sqlalchemy.ext.asyncio import AsyncSession @pytest_asyncio.fixture async def fanout_setup(db_session: AsyncSession) -> AsyncIterator[dict]: system = AgentTable( id=uuid4(), name="System", slug=f"system-{uuid4().hex[:8]}", role=AgentRole.SYSTEM, team=None, status=AgentStatus.ACTIVE, model_config={}, system_prompt="s", capabilities=[], permissions={}, metrics={}, ) fe_dev = AgentTable( id=uuid4(), name="FE Dev", slug=f"fe-dev-{uuid4().hex[:8]}", role=AgentRole.DEVELOPER, team=Team.FRONTEND, status=AgentStatus.ACTIVE, model_config={}, system_prompt="s", capabilities=[], permissions={}, metrics={}, ) db_session.add_all([system, fe_dev]) await db_session.flush() fe_project = ProjectTable( id=uuid4(), name="FE", slug=f"fe-{uuid4().hex[:6]}", git_url="https://example.com/fe.git", assigned_cell=Team.FRONTEND, created_by=system.id, ) ux_project = ProjectTable( id=uuid4(), name="UX", slug=f"ux-{uuid4().hex[:6]}", git_url="https://example.com/ux.git", assigned_cell=Team.UX_UI, created_by=system.id, ) product = ProductTable( id=uuid4(), name="Prod", slug=f"prod-{uuid4().hex[:6]}", created_by=system.id, ) db_session.add_all([fe_project, ux_project, product]) await db_session.flush() svc = TaskService(db_session) choreo = Choreographer( ChoreographerDeps( task=svc, work_session=None, git=None, a2a=None, journal=None, audit=None, evidence_repo=None, ) ) yield { "svc": svc, "choreo": choreo, "creator": system.id, "fe_dev_id": fe_dev.id, "fe_project_id": fe_project.id, "ux_project_id": ux_project.id, "product_id": product.id, } async def _build_product_fanout(setup: dict) -> dict: """Product root with FE + UX cell tasks as siblings, FE waiting on UX.""" svc: TaskService = setup["svc"] choreo: Choreographer = setup["choreo"] root = await svc.create( TaskCreateRequest( title="Build the feature (board fan-out)", description="a real coordination task description over twenty chars", acceptance_criteria=["delegated to frontend + ux_ui cells"], team=Team.BOARD, created_by=setup["creator"], project_id=None, product_id=setup["product_id"], task_type=TaskType.CODE, nature=TaskNature.NON_TECHNICAL, estimated_complexity=Complexity.HIGH, ) ) ux_cell = await svc.create_subtask( TaskCreateRequest( title="UX/UI design for the feature", description="a real ux design task description over twenty chars", acceptance_criteria=["wireframes approved"], team=Team.UX_UI, created_by=setup["creator"], project_id=setup["ux_project_id"], product_id=setup["product_id"], parent_task_id=root.id, task_type=TaskType.DESIGN, nature=TaskNature.TECHNICAL, estimated_complexity=Complexity.MEDIUM, ) ) fe_cell = await svc.create_subtask( TaskCreateRequest( title="Frontend implementation for the feature", description="a real frontend cell task description over twenty chars", acceptance_criteria=["UI matches the design"], team=Team.FRONTEND, created_by=setup["creator"], project_id=setup["fe_project_id"], product_id=setup["product_id"], parent_task_id=root.id, task_type=TaskType.CODE, nature=TaskNature.TECHNICAL, estimated_complexity=Complexity.MEDIUM, ) ) # Cross-cell sequencing: wire the frontend CELL task onto the UX cell task # exactly as the product fan-out does on delegate. await choreo._wire_ux_frontend_dependency(fe_cell, root) await svc.session.flush() refreshed_fe = await svc.get(fe_cell.id) assert refreshed_fe is not None assert ux_cell.id in refreshed_fe.dependency_ids, ( "precondition: frontend cell task must depend on the UX cell task" ) return {"root": root, "ux_cell": ux_cell, "fe_cell": fe_cell} @pytest.mark.asyncio async def test_dev_subtask_held_until_ux_dependency_resolves( fanout_setup: dict, ) -> None: svc: TaskService = fanout_setup["svc"] choreo: Choreographer = fanout_setup["choreo"] tree = await _build_product_fanout(fanout_setup) fe_cell = tree["fe_cell"] ux_cell = tree["ux_cell"] # Cell PM delegates a dev/code subtask under the frontend cell task. dev_subtask = await svc.create_subtask( TaskCreateRequest( title="Implement the login form component", description="a real dev subtask description over twenty chars long", acceptance_criteria=["form renders and submits"], team=Team.FRONTEND, created_by=fanout_setup["creator"], project_id=fanout_setup["fe_project_id"], product_id=fanout_setup["product_id"], parent_task_id=fe_cell.id, assigned_to=fanout_setup["fe_dev_id"], task_type=TaskType.CODE, nature=TaskNature.TECHNICAL, estimated_complexity=Complexity.MEDIUM, ) ) assert dev_subtask.status == TaskStatus.PENDING # The delegate flow wires cross-cell sequencing on the new subtask. await choreo._wire_ux_frontend_dependency(dev_subtask, fe_cell) await svc.session.flush() # While the UX cell task is unresolved, the dev subtask must NOT be # dispatchable (held by the dependency filter). pending = await svc.list_pending(team=Team.FRONTEND, filter_by_dependencies=True) pending_ids = {t.id for t in pending} assert dev_subtask.id not in pending_ids, ( "dev subtask must be held while the UX dependency is unresolved" ) # UX finishes -> terminal state. ux_row = await svc.get(ux_cell.id) assert ux_row is not None ux_row.status = TaskStatus.COMPLETED await svc.session.flush() # Now the dev subtask is dispatchable. pending_after = await svc.list_pending( team=Team.FRONTEND, filter_by_dependencies=True ) pending_after_ids = {t.id for t in pending_after} assert dev_subtask.id in pending_after_ids, ( "dev subtask must become dispatchable once UX reaches a terminal state" )