mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
The coordination/fan-out root carries a product (cell->repo map) but no
project of its own, and was forced branchless — so a cell's parent-branch
resolution fell back to the project default (master), and cell completion
merged each cell straight to master, bypassing the Main-PM integration
point and the CEO merge gate.
Per the locked branch model (master <- feature/main_pm/{root} <- cell <-
dev), the root is now the Main-PM integration point: on claim it cuts
feature/main_pm/{root} off master in EACH distinct repo the product spans
(monorepo => 1, multi-repo => N). Cells then branch off it via the existing
ancestor-branch resolution, so cell work never targets master.
- ProductService.distinct_project_ids: enumerate the repos a product spans
- TaskService._create_branch_in_project: project-parameterized branch
creation split out of _auto_create_branch
- TaskService._ensure_coordination_root_branches: cut the integration
branch in each repo; graceful empty when the product has no cell map yet
- _ensure_branch_for_task routes a product-backed root here, not to no-op
Co-authored-by: Renn F <rennf93@users.noreply.github.com>
182 lines
5.7 KiB
Python
182 lines
5.7 KiB
Python
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING
|
|
from uuid import uuid4
|
|
|
|
import pytest
|
|
import pytest_asyncio
|
|
from roboco.db.tables import AgentTable, ProjectTable
|
|
from roboco.foundation.identity import Team
|
|
from roboco.models import AgentRole, AgentStatus
|
|
from roboco.models.product import ProductCellMapping, ProductCreate, ProductUpdate
|
|
from roboco.services.base import ConflictError
|
|
from roboco.services.product import ProductService
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import AsyncIterator
|
|
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
|
|
@pytest_asyncio.fixture
|
|
async def product_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={},
|
|
)
|
|
db_session.add(system)
|
|
await db_session.flush()
|
|
projects = {}
|
|
for cell in (Team.BACKEND, Team.FRONTEND, Team.UX_UI):
|
|
p = ProjectTable(
|
|
id=uuid4(),
|
|
name=cell.value,
|
|
slug=f"{cell.value}-{uuid4().hex[:6]}",
|
|
git_url="https://example.com/r.git",
|
|
assigned_cell=cell,
|
|
created_by=system.id,
|
|
)
|
|
db_session.add(p)
|
|
projects[cell] = p
|
|
await db_session.flush()
|
|
yield {
|
|
"svc": ProductService(db_session),
|
|
"creator": system.id,
|
|
"projects": projects,
|
|
}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_with_cells_and_project_for(product_setup: dict) -> None:
|
|
svc = product_setup["svc"]
|
|
projects = product_setup["projects"]
|
|
product = await svc.create(
|
|
ProductCreate(
|
|
name="RoboCo",
|
|
slug=f"roboco-{uuid4().hex[:6]}",
|
|
cells=[
|
|
ProductCellMapping(
|
|
team=Team.BACKEND, project_id=projects[Team.BACKEND].id
|
|
),
|
|
ProductCellMapping(
|
|
team=Team.FRONTEND, project_id=projects[Team.FRONTEND].id
|
|
),
|
|
],
|
|
),
|
|
created_by=product_setup["creator"],
|
|
)
|
|
assert await svc.project_for(product.id, Team.BACKEND) == projects[Team.BACKEND].id
|
|
assert (
|
|
await svc.project_for(product.id, Team.FRONTEND) == projects[Team.FRONTEND].id
|
|
)
|
|
# missing-team mapping -> None (graceful fallback happens in the caller)
|
|
assert await svc.project_for(product.id, Team.UX_UI) is None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_shared_project_across_cells(product_setup: dict) -> None:
|
|
"""Monorepo: every cell maps to the same Project."""
|
|
svc = product_setup["svc"]
|
|
shared = product_setup["projects"][Team.BACKEND].id
|
|
product = await svc.create(
|
|
ProductCreate(
|
|
name="Mono",
|
|
slug=f"mono-{uuid4().hex[:6]}",
|
|
cells=[
|
|
ProductCellMapping(team=c, project_id=shared)
|
|
for c in (Team.BACKEND, Team.FRONTEND, Team.UX_UI)
|
|
],
|
|
),
|
|
created_by=product_setup["creator"],
|
|
)
|
|
for c in (Team.BACKEND, Team.FRONTEND, Team.UX_UI):
|
|
assert await svc.project_for(product.id, c) == shared
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_distinct_project_ids_monorepo_and_multirepo(product_setup: dict) -> None:
|
|
"""One integration branch per DISTINCT repo: monorepo => 1, multi-repo => N."""
|
|
svc = product_setup["svc"]
|
|
projects = product_setup["projects"]
|
|
shared = projects[Team.BACKEND].id
|
|
|
|
mono = await svc.create(
|
|
ProductCreate(
|
|
name="Mono",
|
|
slug=f"mono-{uuid4().hex[:6]}",
|
|
cells=[
|
|
ProductCellMapping(team=c, project_id=shared)
|
|
for c in (Team.BACKEND, Team.FRONTEND, Team.UX_UI)
|
|
],
|
|
),
|
|
created_by=product_setup["creator"],
|
|
)
|
|
assert await svc.distinct_project_ids(mono.id) == [shared]
|
|
|
|
multi = await svc.create(
|
|
ProductCreate(
|
|
name="Multi",
|
|
slug=f"multi-{uuid4().hex[:6]}",
|
|
cells=[
|
|
ProductCellMapping(team=c, project_id=projects[c].id)
|
|
for c in (Team.BACKEND, Team.FRONTEND, Team.UX_UI)
|
|
],
|
|
),
|
|
created_by=product_setup["creator"],
|
|
)
|
|
assert set(await svc.distinct_project_ids(multi.id)) == {
|
|
projects[Team.BACKEND].id,
|
|
projects[Team.FRONTEND].id,
|
|
projects[Team.UX_UI].id,
|
|
}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_duplicate_slug_conflicts(product_setup: dict) -> None:
|
|
svc = product_setup["svc"]
|
|
slug = f"dup-{uuid4().hex[:6]}"
|
|
await svc.create(
|
|
ProductCreate(name="A", slug=slug), created_by=product_setup["creator"]
|
|
)
|
|
with pytest.raises(ConflictError):
|
|
await svc.create(
|
|
ProductCreate(name="B", slug=slug), created_by=product_setup["creator"]
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_replaces_cells(product_setup: dict) -> None:
|
|
svc = product_setup["svc"]
|
|
projects = product_setup["projects"]
|
|
product = await svc.create(
|
|
ProductCreate(
|
|
name="U",
|
|
slug=f"u-{uuid4().hex[:6]}",
|
|
cells=[
|
|
ProductCellMapping(
|
|
team=Team.BACKEND, project_id=projects[Team.BACKEND].id
|
|
)
|
|
],
|
|
),
|
|
created_by=product_setup["creator"],
|
|
)
|
|
await svc.update(
|
|
product.id,
|
|
ProductUpdate(
|
|
cells=[
|
|
ProductCellMapping(team=Team.UX_UI, project_id=projects[Team.UX_UI].id)
|
|
]
|
|
),
|
|
)
|
|
assert await svc.project_for(product.id, Team.BACKEND) is None
|
|
assert await svc.project_for(product.id, Team.UX_UI) == projects[Team.UX_UI].id
|