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

96 lines
3.5 KiB
Python
Raw Normal View History

"""Dev dispatch sequence pacing — no doomed spawn for a sequence-held task.
``_spawn_pending_dev`` boots a full dev container for a pre-assigned pending
task. Before this guard, a task held only by the assignee-blind sequence
guard (a non-terminal lower-sequence same-parent sibling, not a declared
dependency) passed every pre-spawn gate — ``_blocked_by_earlier_lane_sibling``
is narrower (same dev's lane) and ``_validate_task_for_spawn`` checks declared
dependencies, not sequence siblings — so the container spawned, the agent's
first claim hit ``_claim_blocked_by_sequence`` at the chokepoint and was
refused, and the agent exited only to be re-spawned next tick. Mirror the PM
path's ``_pending_claim_blocked`` prefilter so a sequence-held task is skipped
pre-spawn instead of churned.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Any, cast
from unittest.mock import AsyncMock, patch
from uuid import uuid4
import pytest
from roboco.runtime.orchestrator import AgentOrchestrator
if TYPE_CHECKING:
import httpx
def _orch() -> AgentOrchestrator:
orch = object.__new__(AgentOrchestrator)
orch._instances = {}
return orch
def _pending_dev_task(**over: Any) -> dict[str, Any]:
base: dict[str, Any] = {
"id": str(uuid4()),
"status": "pending",
"team": "backend",
"task_type": "code",
"title": "Sequenced code leaf",
"assigned_to": str(uuid4()),
}
base.update(over)
return base
@pytest.mark.asyncio
async def test_sequence_held_dev_task_not_spawned() -> None:
"""A sequence-held task skips spawn entirely — no container, no churn."""
orch = _orch()
task = _pending_dev_task()
client = cast("httpx.AsyncClient", object())
with (
patch.object(orch, "_is_agent_active", return_value=False),
patch.object(orch, "_pending_claim_blocked", new=AsyncMock(return_value=True)),
patch.object(orch, "_blocked_by_earlier_lane_sibling", new=AsyncMock()) as lane,
patch.object(orch, "_pm_respawn_should_gate", new=AsyncMock()) as respawn,
patch.object(orch, "spawn_agent", new=AsyncMock()) as spawn,
):
await orch._spawn_pending_dev(client, task, "be-dev-1")
spawn.assert_not_awaited()
# Sequence guard is broader and checked first — narrower per-dev probe never runs.
lane.assert_not_awaited()
respawn.assert_not_awaited()
@pytest.mark.asyncio
async def test_ready_dev_task_still_spawns() -> None:
"""A clear task (prefilter False, lane clear, respawn gate clear, valid)
proceeds to spawn unchanged."""
orch = _orch()
task = _pending_dev_task()
client = cast("httpx.AsyncClient", object())
with (
patch.object(orch, "_is_agent_active", return_value=False),
patch.object(orch, "_pending_claim_blocked", new=AsyncMock(return_value=False)),
patch.object(
orch, "_blocked_by_earlier_lane_sibling", new=AsyncMock(return_value=False)
),
patch.object(
orch, "_pm_respawn_should_gate", new=AsyncMock(return_value=False)
),
patch.object(
orch, "_validate_task_for_spawn", new=AsyncMock(return_value=None)
),
patch.object(
orch, "_get_prompt_for_agent", new=AsyncMock(return_value="prompt")
),
patch.object(orch, "_task_git_context", return_value=None),
patch.object(orch, "spawn_agent", new=AsyncMock()) as spawn,
):
await orch._spawn_pending_dev(client, task, "be-dev-1")
spawn.assert_awaited_once()