mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
feat(megatask): per-cell project map root-subtasks (multi-project, multi-cell)
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.
This commit is contained in:
@@ -993,10 +993,66 @@ async def test_ensure_branch_coordination_root_no_cell_map_stays_branchless() ->
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ensure_branch_raises_when_neither_project_nor_product() -> None:
|
||||
"""A task with neither a project nor a product is genuinely misconfigured."""
|
||||
async def test_ensure_branch_cell_map_root_cuts_integration_branch_per_project() -> (
|
||||
None
|
||||
):
|
||||
"""An ad-hoc cell_projects root cuts feature/main_pm/{root} in each distinct
|
||||
project the map spans — the product-root path with the map sourced from the
|
||||
task instead of a Product."""
|
||||
svc = TaskService(MagicMock())
|
||||
task = MagicMock(branch_name=None, project_id=None, product_id=None)
|
||||
be_proj, fe_proj = uuid4(), uuid4()
|
||||
cell_map = [
|
||||
SimpleNamespace(team=Team.BACKEND, project_id=be_proj),
|
||||
SimpleNamespace(team=Team.FRONTEND, project_id=fe_proj),
|
||||
]
|
||||
task = MagicMock(
|
||||
branch_name=None,
|
||||
project_id=None,
|
||||
product_id=None,
|
||||
batch_id=uuid4(),
|
||||
parent_task_id=uuid4(),
|
||||
cell_projects=cell_map,
|
||||
)
|
||||
create_in_project = AsyncMock(return_value="feature/main_pm/root1234")
|
||||
_bind(svc, "_create_branch_in_project", create_in_project)
|
||||
project_svc = MagicMock(get=AsyncMock(return_value=MagicMock()))
|
||||
with patch("roboco.services.project.get_project_service", return_value=project_svc):
|
||||
result = await svc._ensure_branch_for_task(task, uuid4())
|
||||
assert result == "feature/main_pm/root1234"
|
||||
# one integration branch per distinct project in the map (here 2 cells, 2 projects)
|
||||
assert create_in_project.await_count == len(cell_map)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_distinct_projects_for_task_dedupes_cell_map_by_project_id() -> None:
|
||||
"""Two cells mapping at the same project (the monorepo case) yield ONE
|
||||
integration branch, not two — mirroring product distinct_project_ids."""
|
||||
svc = TaskService(MagicMock())
|
||||
shared = uuid4()
|
||||
task = MagicMock(
|
||||
project_id=None,
|
||||
product_id=None,
|
||||
cell_projects=[
|
||||
SimpleNamespace(team=Team.FRONTEND, project_id=shared),
|
||||
SimpleNamespace(team=Team.BACKEND, project_id=shared),
|
||||
],
|
||||
)
|
||||
ids = await svc._distinct_projects_for_task(task)
|
||||
assert ids == [shared]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ensure_branch_raises_when_neither_project_nor_product() -> None:
|
||||
"""A task with neither a project, a product, nor a cell map is misconfigured."""
|
||||
svc = TaskService(MagicMock())
|
||||
task = MagicMock(
|
||||
branch_name=None,
|
||||
project_id=None,
|
||||
product_id=None,
|
||||
cell_projects=[],
|
||||
batch_id=None,
|
||||
parent_task_id=None,
|
||||
)
|
||||
with pytest.raises(ValueError, match="project_id"):
|
||||
await svc._ensure_branch_for_task(task, uuid4())
|
||||
|
||||
|
||||
Reference in New Issue
Block a user