Files
roboco/tests/unit/gateway/test_spine_cap_cross_team.py
T
Renn F 5da909d9d7 fix(gateway): cross-team planning fanout + complete tracing-gap hints
Task #157 — spine-cap allows planning fanout across cells:
    main-pm's pattern is to delegate planning to be-pm / fe-pm / ux-pm
    in parallel — each on a different team. The previous spine-cap
    rejected all planning siblings under one parent as
    over-decomposition. New helper _is_cross_team_planning skips the
    cap for planning when both teams are non-empty and distinct. Code
    / documentation stay capped regardless (single repo on one branch
    shouldn't have two simultaneous code subtasks).

Task #159 — tracing-gap remediate hints every requirement:
    journal:during_work>=1, journal:struggle, commits>=1, pr_open,
    and self_verified had no entries in _hint_for_missing_key, so when
    they were missing the agent saw the token in `missing[]` but the
    `remediate` text had no instruction for how to satisfy them.
    Smoke-10's be-dev-1 burned multiple turns retrying i_am_done not
    knowing scope='reflect' doesn't count toward during_work. Now
    every token has a hint, the during_work hint warns that reflect
    doesn't satisfy it, and multi-hint remediate uses a numbered list
    so the model treats each requirement as a distinct step instead
    of a semicolon-blob.

Also coerce convert_plan._coerce_risk formatting (ruff-format follow-up
to 9cd73d0).
2026-05-15 08:21:08 +02:00

129 lines
4.4 KiB
Python

"""Task #157: spine-cap allows cross-team planning fanout.
Pre-fix:
main_pm delegates a planning subtask to be-pm (backend cell). When
it then tries to delegate a second planning subtask to fe-pm
(frontend cell), the spine-cap rejects because there's already a
non-terminal task_type='planning' under the parent. Pre-gateway
allowed this parallel cross-cell pattern; the gateway over-applied
the over-decomposition cap.
Fix:
`_sibling_dup_envelope` skips the spine-cap when:
- new task_type == "planning"
- new team != sibling team (both non-empty)
Other combinations stay capped:
- same-team planning: still over-decomposition (real bug)
- code / documentation regardless of team: a single repo on one
branch shouldn't have two simultaneous code subtasks
"""
from __future__ import annotations
from unittest.mock import MagicMock
from roboco.services.gateway.choreographer._impl import Choreographer
def _sibling(*, task_type: str, team: str, status: str = "pending") -> MagicMock:
sib = MagicMock()
sib.id = "11111111-aaaa-bbbb-cccc-dddddddddddd"
sib.status = status
sib.task_type = task_type
sib.team = team
sib.assigned_to = "some-pm"
return sib
def test_cross_team_planning_fanout_is_allowed() -> None:
"""main-pm: planning→be-pm (backend) exists; planning→fe-pm (frontend) must pass."""
sib = _sibling(task_type="planning", team="backend")
env = Choreographer._sibling_dup_envelope(
sibling=sib,
new_type="planning",
new_team="frontend",
new_assignee="fe-pm",
)
assert env is None, (
"Cross-team planning fanout (backend↔frontend) must NOT be rejected. "
f"Got envelope: {env}"
)
def test_cross_team_planning_third_cell_also_allowed() -> None:
"""Third cell (ux_ui) is also a valid cross-team planning fanout target."""
sib = _sibling(task_type="planning", team="backend")
env = Choreographer._sibling_dup_envelope(
sibling=sib,
new_type="planning",
new_team="ux_ui",
new_assignee="ux-pm",
)
assert env is None, env
def test_same_team_planning_still_rejected() -> None:
"""Two planning subtasks on the SAME team is the real over-decomp pattern —
must still be blocked by the spine-cap."""
sib = _sibling(task_type="planning", team="backend")
env = Choreographer._sibling_dup_envelope(
sibling=sib,
new_type="planning",
new_team="backend",
new_assignee="be-pm",
)
assert env is not None
body = env.as_dict()
assert body["error"] == "invalid_state", body
def test_cross_team_code_still_rejected() -> None:
"""Code subtasks stay capped regardless of team — only one code task per
parent at a time. (Cross-team code under one parent is meaningless;
each cell's code work lives under its own cell-PM planning task.)"""
sib = _sibling(task_type="code", team="backend")
env = Choreographer._sibling_dup_envelope(
sibling=sib,
new_type="code",
new_team="frontend",
new_assignee="fe-dev-1",
)
assert env is not None
body = env.as_dict()
assert body["error"] == "invalid_state", body
def test_cross_team_documentation_still_rejected() -> None:
"""Documentation subtasks stay capped regardless of team — single doc
pass per parent."""
sib = _sibling(task_type="documentation", team="backend")
env = Choreographer._sibling_dup_envelope(
sibling=sib,
new_type="documentation",
new_team="frontend",
new_assignee="fe-doc",
)
assert env is not None
def test_planning_with_missing_team_still_rejected() -> None:
"""If either side has no team attribute (defensive), fall back to the
strict cap. Don't let an empty-team escape hatch sneak past."""
sib_no_team = _sibling(task_type="planning", team="")
env = Choreographer._sibling_dup_envelope(
sibling=sib_no_team,
new_type="planning",
new_team="backend",
new_assignee="be-pm",
)
assert env is not None, "Empty team on sibling must NOT bypass the cap"
sib = _sibling(task_type="planning", team="backend")
env2 = Choreographer._sibling_dup_envelope(
sibling=sib,
new_type="planning",
new_team="",
new_assignee="be-pm",
)
assert env2 is not None, "Empty team on new task must NOT bypass the cap"