Files
roboco/tests/unit/runtime/test_readiness_role_status_match.py
T
Renn F eec35c0357 fix(orchestrator): un-deadlock a CEO-rejected coordination root
A coordination root (team=main_pm, product-linked, no repo) the CEO sends back
lands in needs_revision, but the dev dispatcher skips it (not a cell team) and
the closure path only handles paused parents — so it sat in needs_revision
forever. (NOT a foundation-spec gap: the spec already allows needs_revision ->
claimed for any role.)

- _dispatch_revision_coordination_roots: re-spawn the owning PM for a
  needs_revision coordination root so it re-coordinates the revision (registered
  in the dispatch loop after PM closure)
- _readiness_check_role_for_status: widen the dev-owned states (needs_revision,
  verifying) to also accept cell_pm/main_pm for coordination roots — a pure
  widening; normal code tasks stay dev/doc-only
- 16 unit tests (dispatcher decision + readiness widening)
2026-06-17 16:50:10 +02:00

133 lines
4.7 KiB
Python

"""Smoke-8: _readiness_check_role_for_status covers dev-owned states.
Original gap: the role-mismatch table only mapped handoff states
(awaiting_qa, awaiting_documentation, awaiting_pm_review,
awaiting_ceo_approval) to required roles. needs_revision/verifying had
no entry, so a QA spawn for a needs_revision task passed the readiness
check and the gateway rejected claim_review afterwards. Defense in depth
layered behind the _check_health fix.
"""
from __future__ import annotations
from roboco.runtime.orchestrator import AgentOrchestrator
def test_qa_on_needs_revision_blocked() -> None:
"""QA cannot be spawned for a needs_revision task."""
reason = AgentOrchestrator._readiness_check_role_for_status(
agent_id="be-qa", role="qa", status="needs_revision"
)
assert reason is not None
assert "needs_revision" in reason
assert "qa" in reason
def test_pm_on_needs_revision_blocked() -> None:
"""PM cannot be spawned for a needs_revision task either."""
reason = AgentOrchestrator._readiness_check_role_for_status(
agent_id="be-pm", role="cell_pm", status="needs_revision"
)
assert reason is not None
def test_developer_on_needs_revision_allowed() -> None:
"""Developer (and documenter) ARE the right roles for needs_revision."""
reason = AgentOrchestrator._readiness_check_role_for_status(
agent_id="be-dev-1", role="developer", status="needs_revision"
)
assert reason is None
def test_documenter_on_needs_revision_allowed() -> None:
"""Documenter can rework — same dev/doc-owned set."""
reason = AgentOrchestrator._readiness_check_role_for_status(
agent_id="be-doc", role="documenter", status="needs_revision"
)
assert reason is None
def test_qa_on_verifying_blocked() -> None:
"""Verifying belongs to the dev/doc roles, not QA."""
reason = AgentOrchestrator._readiness_check_role_for_status(
agent_id="be-qa", role="qa", status="verifying"
)
assert reason is not None
def test_qa_on_awaiting_qa_allowed() -> None:
"""The original handoff case still works — QA on awaiting_qa is fine."""
reason = AgentOrchestrator._readiness_check_role_for_status(
agent_id="be-qa", role="qa", status="awaiting_qa"
)
assert reason is None
def test_unmapped_status_allows_any_role() -> None:
"""Statuses with no role lock (pending, paused, blocked, etc.) pass."""
for status in ("pending", "claimed", "in_progress", "paused", "blocked"):
for role in ("developer", "qa", "documenter", "cell_pm"):
assert (
AgentOrchestrator._readiness_check_role_for_status(
agent_id="x", role=role, status=status
)
is None
), f"role={role} status={status} should not be rejected by this gate"
# ---------------------------------------------------------------------------
# Coordination roots — a CEO-rejected coordination root returns to its PM (#5).
# ---------------------------------------------------------------------------
def test_pm_on_needs_revision_coordination_allowed() -> None:
"""A coordination root in needs_revision belongs to its PM, not a dev."""
for role in ("main_pm", "cell_pm"):
assert (
AgentOrchestrator._readiness_check_role_for_status(
agent_id="main-pm",
role=role,
status="needs_revision",
is_coordination=True,
)
is None
)
def test_pm_on_verifying_coordination_allowed() -> None:
assert (
AgentOrchestrator._readiness_check_role_for_status(
agent_id="main-pm", role="main_pm", status="verifying", is_coordination=True
)
is None
)
def test_dev_on_needs_revision_coordination_still_allowed() -> None:
"""Widening is additive — developer is still accepted."""
assert (
AgentOrchestrator._readiness_check_role_for_status(
agent_id="be-dev-1",
role="developer",
status="needs_revision",
is_coordination=True,
)
is None
)
def test_qa_on_needs_revision_coordination_still_blocked() -> None:
"""The widening only adds the PM roles — QA is still a misroute."""
reason = AgentOrchestrator._readiness_check_role_for_status(
agent_id="be-qa", role="qa", status="needs_revision", is_coordination=True
)
assert reason is not None
def test_pm_on_needs_revision_noncoordination_still_blocked() -> None:
"""A normal (code) needs_revision task is still dev/doc-only for a PM."""
reason = AgentOrchestrator._readiness_check_role_for_status(
agent_id="be-pm", role="cell_pm", status="needs_revision", is_coordination=False
)
assert reason is not None