Files
roboco/tests/unit/runtime/test_self_heal_dispatch_inert.py
T
Renn F 7ef7d8414e fix(self-heal): hold an unconfirmed fix task out of dispatch until CEO approval
Adversarial review found the load-bearing invariant broken at the dispatch
layer: _dispatch_pm_work skipped only PR_REVIEW_SOURCES, so a PENDING
team=main_pm self_heal task (assigned_to=None, confirmed_by_human=False) was
routed to Main PM and spawned BEFORE the CEO approved it — the "never start
until you approve" promise didn't hold.

Fix: the PM dispatcher now also skips source='self_heal' while
confirmed_by_human is False (before the assigned/unassigned split, so it holds
either way); the task still shows in the panel so the CEO can see and approve
it. approve_and_start flips confirmed_by_human=True (the CEO's start IS the
human confirmation), so it dispatches normally afterward. Other sources are
unaffected.

Tests: a unit test that the dispatcher holds an unconfirmed self_heal task but
routes a confirmed one and ordinary tasks, plus a DB test that approve_and_start
flips the gate. (The readiness gate was deliberately not used — a blocker there
marks the task `blocked`; the dispatch skip leaves it cleanly PENDING.)
2026-06-17 21:55:33 +02:00

45 lines
1.6 KiB
Python

"""The PM dispatcher holds an unconfirmed self-heal task OUT of dispatch.
The load-bearing invariant: a source='self_heal' task the loop opened must NOT
be routed / claimed / spawned while confirmed_by_human is False — it sits inert
until the CEO Approve-&-Starts it (which flips the flag). Once confirmed it
routes like any other task. Mirrors how PR-review tasks are skipped.
"""
from __future__ import annotations
from typing import Any, cast
from unittest.mock import AsyncMock, MagicMock
import pytest
from roboco.runtime.orchestrator import AgentOrchestrator
def _task(tid: str, source: str, confirmed: bool) -> dict[str, Any]:
return {
"id": tid,
"source": source,
"confirmed_by_human": confirmed,
"assigned_to": None,
}
@pytest.mark.asyncio
async def test_unconfirmed_self_heal_task_is_held_out_of_dispatch() -> None:
tasks = [
_task("A", "self_heal", False), # held — must NOT route until approved
_task("B", "self_heal", True), # CEO-approved → routes
_task("C", "manual", False), # ordinary task → routes
]
stub = MagicMock()
stub._fetch_tasks = AsyncMock(return_value=tasks)
stub._is_task_handled_this_tick = MagicMock(return_value=False)
stub._route_unassigned_pm_task = AsyncMock()
client: Any = MagicMock()
await AgentOrchestrator._dispatch_pm_work(cast("AgentOrchestrator", stub), client)
routed = [c.args[1]["id"] for c in stub._route_unassigned_pm_task.await_args_list]
assert "A" not in routed # the unconfirmed self-heal task stays inert
assert set(routed) == {"B", "C"}