"""Choreographer._resolve_subtask_project — fan-out resolution coverage. The method resolves which project a delegated subtask lands in. It has four priority tiers, exercised here against a stub ``self`` (the cell-map match path never touches ``self``, and the raise path only reads ``self.product``): 1. explicit ``inputs.project_id`` wins outright. 2. the parent's ad-hoc ``cell_projects`` map → the mapping for ``inputs.team``. 3. the parent's Product map (delegated to ``self.product.project_for``). 4. the parent's own ``project_id``. 5. otherwise ``TaskCompletenessError`` (no repo to land in). The ad-hoc cell-map tier (2) is the multi-cell MegaTask root-subtask seam; the other tiers are unchanged product-root / single-project behavior. """ from __future__ import annotations from types import SimpleNamespace from typing import Any from uuid import UUID, uuid4 import pytest from roboco.foundation.policy.task_completeness import TaskCompletenessError from roboco.models.base import Team from roboco.services.gateway.choreographer._impl import ( Choreographer, DelegateInputs, ) def _inputs(*, team: Team, project_id: UUID | None = None) -> DelegateInputs: return DelegateInputs( title="t", description="d", assigned_to="be-dev-1", team=team.value, task_type="code", nature="technical", acceptance_criteria=["a"], project_id=project_id, ) def _mapping(team: Team, project_id: UUID) -> SimpleNamespace: return SimpleNamespace(team=team, project_id=project_id) @pytest.mark.asyncio async def test_explicit_inputs_project_id_wins_over_cell_map() -> None: """Tier 1: an explicit project_id on delegate short-circuits the map.""" be_proj, explicit = uuid4(), uuid4() parent = SimpleNamespace( cell_projects=[_mapping(Team.BACKEND, be_proj)], product_id=None, project_id=None, ) self_stub: Any = SimpleNamespace(product=None) resolved = await Choreographer._resolve_subtask_project( self_stub, parent, _inputs(team=Team.BACKEND, project_id=explicit) ) assert resolved == explicit @pytest.mark.asyncio async def test_cell_map_resolves_project_for_matching_team() -> None: """Tier 2: the parent's cell map yields the project for the delegated cell.""" be_proj, fe_proj = uuid4(), uuid4() parent = SimpleNamespace( cell_projects=[ _mapping(Team.FRONTEND, fe_proj), _mapping(Team.BACKEND, be_proj), ], product_id=None, project_id=None, ) self_stub: Any = SimpleNamespace(product=None) resolved = await Choreographer._resolve_subtask_project( self_stub, parent, _inputs(team=Team.BACKEND) ) assert resolved == be_proj @pytest.mark.asyncio async def test_cell_map_missing_team_falls_through_to_parent_project() -> None: """No mapping for the requested cell → fall through to the parent's own project_id (tier 4), not raise — the parent may still carry a project.""" be_proj, own = uuid4(), uuid4() parent = SimpleNamespace( cell_projects=[_mapping(Team.BACKEND, be_proj)], product_id=None, project_id=own, ) self_stub: Any = SimpleNamespace(product=None) # Frontend subtask but the map only covers backend → fall to parent.project_id. resolved = await Choreographer._resolve_subtask_project( self_stub, parent, _inputs(team=Team.FRONTEND) ) assert resolved == own @pytest.mark.asyncio async def test_cell_map_only_parent_with_no_match_raises_completeness() -> None: """A fan-out parent (cell map, no own project, no product) with no mapping for the requested cell raises TaskCompletenessError — the subtask has no repo to land in.""" be_proj = uuid4() parent = SimpleNamespace( cell_projects=[_mapping(Team.BACKEND, be_proj)], product_id=None, project_id=None, ) self_stub: Any = SimpleNamespace(product=None) with pytest.raises(TaskCompletenessError) as exc: await Choreographer._resolve_subtask_project( self_stub, parent, _inputs(team=Team.FRONTEND) ) assert "project_id" in exc.value.missing