mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
The toolchain guard fails open on a recorded 'unknown' status (precision over recall — never strand a task on an inconclusive smoke). But an 'unknown' means provisioning ran yet the smoke could not confirm the suite is collectable under the interpreter, so the gate was proceeding blind with no trace — a silent hollow pass. It now emits a 'toolchain.unverified_gate_pass' warning with the agent and task ids when proceeding past 'unknown', while still not blocking; a missing marker (None) stays silent so the warning carries signal.
96 lines
3.7 KiB
Python
96 lines
3.7 KiB
Python
"""The loud-fail guard: a delivery gate refuses when the suite can't run.
|
|
|
|
When toolchain matching is on and the acting agent's workspace recorded a
|
|
``broken`` toolchain status (the project's suite cannot be collected under the
|
|
provisioned interpreter), the dev/QA/PR gates must block — never let a role
|
|
"pass" on a source read. Off, or any non-broken / unknown status, never blocks.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
from uuid import uuid4
|
|
|
|
import pytest
|
|
from roboco.config import settings
|
|
from roboco.services.gateway.choreographer import Choreographer, ChoreographerDeps
|
|
from structlog.testing import capture_logs
|
|
|
|
|
|
def _make_choreographer(*, status: str | None) -> Choreographer:
|
|
base: dict[str, Any] = {
|
|
"task": AsyncMock(),
|
|
"work_session": AsyncMock(),
|
|
"git": AsyncMock(),
|
|
"a2a": AsyncMock(),
|
|
"journal": AsyncMock(),
|
|
"audit": AsyncMock(),
|
|
"evidence_repo": AsyncMock(),
|
|
}
|
|
base["git"].toolchain_status_for_task.return_value = status
|
|
return Choreographer(ChoreographerDeps(**base))
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_guard_blocks_when_broken_and_flag_on(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
monkeypatch.setattr(settings, "toolchain_match_enabled", True)
|
|
c = _make_choreographer(status="broken")
|
|
env = await c._toolchain_broken_guard(uuid4(), MagicMock())
|
|
assert env is not None
|
|
body = env.as_dict()
|
|
assert body["error"] == "invalid_state"
|
|
assert "i_am_blocked" in body["remediate"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_guard_passes_when_ok(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setattr(settings, "toolchain_match_enabled", True)
|
|
c = _make_choreographer(status="ok")
|
|
assert await c._toolchain_broken_guard(uuid4(), MagicMock()) is None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_guard_passes_when_status_unknown_or_missing(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
monkeypatch.setattr(settings, "toolchain_match_enabled", True)
|
|
for status in ("unknown", None):
|
|
c = _make_choreographer(status=status)
|
|
assert await c._toolchain_broken_guard(uuid4(), MagicMock()) is None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_guard_inert_when_flag_off(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setattr(settings, "toolchain_match_enabled", False)
|
|
c = _make_choreographer(status="broken")
|
|
assert await c._toolchain_broken_guard(uuid4(), MagicMock()) is None
|
|
# Flag off => the workspace is never consulted at all.
|
|
c.git.toolchain_status_for_task.assert_not_awaited()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_guard_warns_loudly_on_unknown(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
# 'unknown' still fails open (never strands a task), but it must not be
|
|
# silent — a warning is emitted so the hollow pass is visible to operators.
|
|
monkeypatch.setattr(settings, "toolchain_match_enabled", True)
|
|
c = _make_choreographer(status="unknown")
|
|
with capture_logs() as logs:
|
|
env = await c._toolchain_broken_guard(uuid4(), MagicMock())
|
|
assert env is None
|
|
assert any(e.get("event") == "toolchain.unverified_gate_pass" for e in logs)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_guard_silent_when_no_marker(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
# No marker (None) is benign — flag on but not yet provisioned / not a test
|
|
# project — and must stay silent so the warning means something.
|
|
monkeypatch.setattr(settings, "toolchain_match_enabled", True)
|
|
c = _make_choreographer(status=None)
|
|
with capture_logs() as logs:
|
|
env = await c._toolchain_broken_guard(uuid4(), MagicMock())
|
|
assert env is None
|
|
assert not any(e.get("event") == "toolchain.unverified_gate_pass" for e in logs)
|