Files
roboco/tests/foundation/test_team_match_context.py
T

105 lines
3.3 KiB
Python
Raw Normal View History

2026-07-02 15:36:49 +02:00
"""Team-match must actually fire: the spec gate rejects cross-team actors.
Live 2026-07-02: a frontend cell PM was dispatched onto a BACKEND task's
review, then blocked it, escalated it, and briefly held it — a 40-minute
ownership tug-of-war. Seventeen ActionSpecs carry needs_team_match=True and
_check_team_match enforces it — but only when the caller supplies
Context.agent_team, which no choreographer site did, so the gate sat in its
permissive fallback forever. These tests pin the policy behavior the
choreographer sweep wires up.
"""
from __future__ import annotations
from types import SimpleNamespace
from typing import Any, cast
from uuid import uuid4
from roboco.foundation.policy.lifecycle import (
Context,
Role,
can_invoke_intent,
)
from roboco.models.base import TaskStatus
def _task(**overrides: Any) -> Any:
base: dict[str, Any] = {
"id": uuid4(),
"status": TaskStatus.IN_PROGRESS,
"team": "backend",
"assigned_to": None,
"task_type": "code",
}
base.update(overrides)
return cast("Any", SimpleNamespace(**base))
def test_cross_team_developer_is_rejected_when_team_supplied() -> None:
decision = can_invoke_intent(
Role.DEVELOPER,
"i_am_blocked",
_task(team="backend", status=TaskStatus.IN_PROGRESS, task_type="code"),
Context(actor_id=uuid4(), agent_team="frontend"),
)
assert not decision.allowed
assert "team" in (decision.message or "").lower()
def test_cross_team_cell_pm_resume_is_rejected() -> None:
decision = can_invoke_intent(
Role.CELL_PM,
"resume",
_task(team="backend", status=TaskStatus.PAUSED),
Context(actor_id=uuid4(), agent_team="frontend"),
)
assert not decision.allowed
assert "team" in (decision.message or "").lower()
def test_same_team_developer_is_allowed() -> None:
decision = can_invoke_intent(
Role.DEVELOPER,
"i_am_blocked",
_task(team="backend", status=TaskStatus.IN_PROGRESS, task_type="code"),
Context(actor_id=uuid4(), agent_team="backend"),
)
assert decision.allowed
def test_missing_team_keeps_permissive_fallback() -> None:
"""Absent agent_team defers to the service layer (backward compatible)."""
decision = can_invoke_intent(
Role.DEVELOPER,
"i_am_blocked",
_task(team="backend", status=TaskStatus.IN_PROGRESS, task_type="code"),
Context(actor_id=uuid4()),
)
assert decision.allowed
def test_org_wide_roles_are_exempt_cross_team() -> None:
"""Main PM handles every cell's escalations; the exemption keeps that."""
for role, verb, status in (
(Role.MAIN_PM, "resume", TaskStatus.PAUSED),
(Role.MAIN_PM, "unblock", TaskStatus.BLOCKED),
):
decision = can_invoke_intent(
role,
verb,
_task(team="backend", status=status),
Context(actor_id=uuid4(), agent_team="main_pm"),
)
assert decision.allowed, f"{role} {verb} must stay org-wide"
def test_cross_team_cell_pm_unblock_is_rejected() -> None:
decision = can_invoke_intent(
Role.CELL_PM,
"unblock",
_task(team="backend", status=TaskStatus.BLOCKED),
Context(actor_id=uuid4(), agent_team="frontend"),
)
assert not decision.allowed
assert "team" in (decision.message or "").lower()