2026-06-17 08:01:49 +02:00
|
|
|
"""required_cells decomposition gate — marker parse + uncovered-cell coverage.
|
|
|
|
|
|
|
|
|
|
The Main PM must create a subtask for each cell the brief explicitly names
|
2026-06-21 02:48:07 +02:00
|
|
|
(recorded as a ``required_cells`` orchestration marker on the parent). The gate
|
|
|
|
|
is inert when no marker is present, so legacy decompositions never block.
|
2026-06-17 08:01:49 +02:00
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
2026-06-21 02:48:07 +02:00
|
|
|
from types import SimpleNamespace
|
2026-06-17 08:01:49 +02:00
|
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
|
from uuid import uuid4
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
from roboco.services.task import TaskService, extract_required_cells
|
|
|
|
|
|
2026-06-21 02:48:07 +02:00
|
|
|
|
|
|
|
|
def _task(required_cells: list[str] | None = None) -> SimpleNamespace:
|
|
|
|
|
om = {"required_cells": required_cells} if required_cells is not None else None
|
|
|
|
|
return SimpleNamespace(orchestration_markers=om)
|
|
|
|
|
|
|
|
|
|
|
2026-06-17 08:01:49 +02:00
|
|
|
# ---------------------------------------------------------------------------
|
2026-06-21 02:48:07 +02:00
|
|
|
# extract_required_cells (marker reader)
|
2026-06-17 08:01:49 +02:00
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_extract_required_cells_absent_is_empty() -> None:
|
2026-06-21 02:48:07 +02:00
|
|
|
assert extract_required_cells(_task()) == []
|
|
|
|
|
assert (
|
|
|
|
|
extract_required_cells(
|
|
|
|
|
SimpleNamespace(orchestration_markers={"original_developer": "abc"})
|
|
|
|
|
)
|
|
|
|
|
== []
|
|
|
|
|
)
|
2026-06-17 08:01:49 +02:00
|
|
|
|
|
|
|
|
|
2026-06-21 02:48:07 +02:00
|
|
|
def test_extract_required_cells_normalizes() -> None:
|
|
|
|
|
assert extract_required_cells(_task(["Backend", "Frontend ", "UX/UI"])) == [
|
|
|
|
|
"backend",
|
|
|
|
|
"frontend",
|
|
|
|
|
"ux_ui",
|
|
|
|
|
]
|
2026-06-17 08:01:49 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_extract_required_cells_dedups_in_order() -> None:
|
2026-06-21 02:48:07 +02:00
|
|
|
assert extract_required_cells(_task(["backend", "backend", "frontend"])) == [
|
|
|
|
|
"backend",
|
|
|
|
|
"frontend",
|
|
|
|
|
]
|
2026-06-17 08:01:49 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# uncovered_required_cells (service coverage check)
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
2026-06-21 02:48:07 +02:00
|
|
|
def _service(
|
|
|
|
|
required_cells: list[str] | None, child_teams: list[str | None]
|
|
|
|
|
) -> TaskService:
|
2026-06-17 08:01:49 +02:00
|
|
|
"""A TaskService whose get()/get_subtasks() return a parent + these children."""
|
|
|
|
|
svc = TaskService(MagicMock())
|
2026-06-21 02:48:07 +02:00
|
|
|
parent = _task(required_cells)
|
2026-06-17 08:01:49 +02:00
|
|
|
children = [MagicMock(team=t) for t in child_teams]
|
|
|
|
|
object.__setattr__(svc, "get", AsyncMock(return_value=parent))
|
|
|
|
|
object.__setattr__(svc, "get_subtasks", AsyncMock(return_value=children))
|
|
|
|
|
return svc
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_uncovered_inert_without_marker() -> None:
|
2026-06-21 02:48:07 +02:00
|
|
|
svc = _service(None, ["backend"])
|
2026-06-17 08:01:49 +02:00
|
|
|
assert await svc.uncovered_required_cells(uuid4()) == []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_uncovered_flags_the_dropped_cell() -> None:
|
|
|
|
|
# Brief named backend+frontend+ux_ui; only backend+frontend got subtasks.
|
2026-06-21 02:48:07 +02:00
|
|
|
svc = _service(["backend", "frontend", "ux_ui"], ["backend", "frontend"])
|
2026-06-17 08:01:49 +02:00
|
|
|
assert await svc.uncovered_required_cells(uuid4()) == ["ux_ui"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_uncovered_empty_when_all_named_cells_covered() -> None:
|
2026-06-21 02:48:07 +02:00
|
|
|
svc = _service(["backend", "frontend"], ["frontend", "backend"])
|
2026-06-17 08:01:49 +02:00
|
|
|
assert await svc.uncovered_required_cells(uuid4()) == []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_uncovered_normalizes_child_team_form() -> None:
|
|
|
|
|
# Marker uses underscore, child team uses the slash form — they match.
|
2026-06-21 02:48:07 +02:00
|
|
|
svc = _service(["ux_ui"], ["UX/UI"])
|
2026-06-17 08:01:49 +02:00
|
|
|
assert await svc.uncovered_required_cells(uuid4()) == []
|