Files
roboco/tests/unit/runtime/test_pr_gate_auto_submit.py
T

144 lines
5.0 KiB
Python
Raw Normal View History

"""The PR-gate turn cut: closure auto-submits assembled parents to the gate.
When every child of an assembled parent is terminal, the orchestrator used
to spawn the PM just to call submit_up/submit_root — a whole agent turn
whose substance (freshness rebase, integrity check, PR open) is
deterministic gate code. ``_try_auto_submit`` runs the REAL submit verb
through the internal API as the owning PM; only a gate rejection falls
back to the classic PM closure spawn. The PM's remaining turn is the one
that needs judgment: the final merge (or the revision).
"""
from __future__ import annotations
from typing import Any
from unittest.mock import AsyncMock, MagicMock
import pytest
from roboco.config import settings as cfg
from roboco.runtime.orchestrator import AGENT_UUIDS, AgentOrchestrator
# The commit/notes validator's minimum substantive length.
_MIN_NOTES = 20
_CELL_TASK: dict[str, Any] = {
"id": "11111111-1111-1111-1111-111111111111",
"team": "backend",
"branch_name": "feature/backend/AAAA1111",
"project_id": "22222222-2222-2222-2222-222222222222",
"assigned_to": "33333333-3333-3333-3333-333333333333",
"status": "in_progress",
}
def _orch() -> AgentOrchestrator:
orch = AgentOrchestrator.__new__(AgentOrchestrator)
orch._tick_handled_tasks = set()
orch._bg_tasks = set()
return orch
def _client(envelope: dict[str, Any]) -> MagicMock:
response = MagicMock()
response.json.return_value = envelope
client = MagicMock()
client.post = AsyncMock(return_value=response)
return client
@pytest.mark.asyncio
async def test_cell_parent_auto_submits_as_owning_pm(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(cfg, "pr_gate_auto_submit_enabled", True)
orch = _orch()
client = _client({"status": "awaiting_pr_review", "error": None})
assert await orch._try_auto_submit(client, _CELL_TASK, "be-pm") is True
(url,), kwargs = client.post.call_args
assert url == f"{orch._api_url}/v1/flow/cell_pm/submit_up"
assert kwargs["headers"]["X-Agent-ID"] == _CELL_TASK["assigned_to"]
assert kwargs["headers"]["X-Agent-Role"] == "cell_pm"
assert kwargs["json"]["task_id"] == _CELL_TASK["id"]
assert len(kwargs["json"]["notes"]) >= _MIN_NOTES
@pytest.mark.asyncio
async def test_main_pm_root_auto_submits_submit_root(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(cfg, "pr_gate_auto_submit_enabled", True)
orch = _orch()
client = _client({"status": "awaiting_pr_review", "error": None})
task = {**_CELL_TASK, "team": "main_pm"}
assert await orch._try_auto_submit(client, task, "main-pm") is True
(url,), kwargs = client.post.call_args
assert url == f"{orch._api_url}/v1/flow/main_pm/submit_root"
assert kwargs["headers"]["X-Agent-Role"] == "main_pm"
@pytest.mark.asyncio
async def test_branchless_parent_never_auto_submits(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""A branchless coordination parent (MegaTask umbrella) assembles no PR."""
monkeypatch.setattr(cfg, "pr_gate_auto_submit_enabled", True)
orch = _orch()
client = _client({"error": None})
task = {**_CELL_TASK, "branch_name": None}
assert await orch._try_auto_submit(client, task, "be-pm") is False
client.post.assert_not_called()
@pytest.mark.asyncio
async def test_flag_off_is_inert(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(cfg, "pr_gate_auto_submit_enabled", False)
orch = _orch()
client = _client({"error": None})
assert await orch._try_auto_submit(client, _CELL_TASK, "be-pm") is False
client.post.assert_not_called()
@pytest.mark.asyncio
async def test_gate_rejection_falls_back_to_pm_spawn(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""A rejection envelope (e.g. integrity/freshness refusal) means the PM
turn is genuinely needed — auto-submit yields to the closure spawn."""
monkeypatch.setattr(cfg, "pr_gate_auto_submit_enabled", True)
orch = _orch()
client = _client(
{"error": "invalid_state", "message": "assembled branch behind base"}
)
assert await orch._try_auto_submit(client, _CELL_TASK, "be-pm") is False
client.post.assert_called_once()
@pytest.mark.asyncio
async def test_missing_assignment_falls_back_to_static_identity(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(cfg, "pr_gate_auto_submit_enabled", True)
orch = _orch()
client = _client({"status": "awaiting_pr_review", "error": None})
task = {**_CELL_TASK, "assigned_to": None}
assert await orch._try_auto_submit(client, task, "be-pm") is True
(_, kwargs) = client.post.call_args
assert kwargs["headers"]["X-Agent-ID"] == AGENT_UUIDS["be-pm"]
@pytest.mark.asyncio
async def test_transport_error_falls_back(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(cfg, "pr_gate_auto_submit_enabled", True)
orch = _orch()
client = MagicMock()
client.post = AsyncMock(side_effect=RuntimeError("api down"))
assert await orch._try_auto_submit(client, _CELL_TASK, "be-pm") is False