mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
A MegaTask root-subtask can now target an ad-hoc per-cell project map — a
third targeting shape that mirrors the existing product fan-out root. In
RoboCo a project is per-cell (ProjectTable.assigned_cell); a monorepo is N
per-cell projects sharing one git_url. So 'multi-cell' IS 'multi-project',
and a task may mix per-cell projects across products or include OSS-library
projects not in any product.
Storage: migration 052 adds task_cell_projects (mirrors product_projects;
unique per (task, team)). TaskTable gains a cascade-delete cell_projects
relationship; TaskCreateRequest / TaskCreate / Task response carry the map.
Policy: batch.is_branchless_coordination + is_valid_batch_shape gain a
has_cell_projects param — a root-subtask targets exactly one of project /
product / cell-map; the umbrella still targets none. TaskService passes
has_cell_projects at every predicate call site and persists the rows in
create(). _ensure_branch_for_task cuts feature/main_pm/{root} per distinct
project in the map (via _distinct_projects_for_task); _require_target_or_umbrella
and _validate_batch_membership accept the map shape.
Fan-out: every distinct_project_ids site (task.py branch creation, routes
_project_for_complete + _resolve_project_for_merge, orchestrator
_ambient_projects_for_task, pr_review._project_slug_for, git._project_for_task)
generalizes to first-distinct-project-of-map-or-product. Choreographer
_resolve_subtask_project resolves a delegated subtask's cell from the parent's
cell map. The product-scoped _slugs_for_product intake helper is unchanged.
Intake: prompter._draft_cell_map extracts the per-cell map from the_work[].
_validate_batch_scope counts distinct projects across all drafts' cells
(>=2 min stays; one 2-cell draft satisfies it). create_task_from_draft
persists cell_projects for >=2-cell drafts (project_id/product_id None),
collapses a 1-cell map to the single-project shape, and leaves single-cell
top-level project_id drafts unchanged. _resolve_owning_team routes a
multi-cell map to Main PM (coordination root, like a product root — a cell
PM can't delegate cross-cell). propose_draft/propose_batch tool descriptions
declare the per-cell project_id (both Claude SDK + grok runtimes).
The umbrella stays branchless / pure-coordination / submit_root-rejected;
the CEO-escalation pr_number gate is not widened (the map root is
is_umbrella=False, mirroring a product root, so submit_root supplies it).
Single-cell root-subtasks and everything below them are byte-for-byte
unchanged. Un-run MegaTask waves (multi-cell drafts) become runnable.
118 lines
4.1 KiB
Python
118 lines
4.1 KiB
Python
"""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 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 = 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 = 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 = 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 = 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
|