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

104 lines
3.4 KiB
Python
Raw Normal View History

"""`_auto_block_task` must be state-aware and never log an empty error.
Live incident: the orchestrator logged `{"error": "", "event": "Failed to
auto-block task"}` for a task whose owning container had died mid-
awaiting_qa — an empty error string with no diagnostic value, from a PATCH
attempting to force a task QA already owns back to "blocked". This pins:
- a task already past dev control (awaiting_qa, terminal, ...) is skipped
with an info log, no PATCH attempted
- a still-blockable task (pending) proceeds to the PATCH as before
- a PATCH failure logs a non-empty error even for exception types whose
str() is empty (e.g. a bare TimeoutError)
- a failed status pre-check does not swallow the block attempt itself
"""
from __future__ import annotations
from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from roboco.runtime.orchestrator import AgentOrchestrator
def _make_orch() -> AgentOrchestrator:
return AgentOrchestrator.__new__(AgentOrchestrator)
def _resp(status_code_ok: bool, payload: dict[str, Any]) -> MagicMock:
r = MagicMock()
r.is_success = status_code_ok
r.json.return_value = payload
return r
@pytest.mark.asyncio
async def test_auto_block_skips_task_already_in_awaiting_qa() -> None:
orch = _make_orch()
client: Any = AsyncMock()
client.get.return_value = _resp(True, {"status": "awaiting_qa"})
await orch._auto_block_task(client, "tid-1", "container died")
client.patch.assert_not_awaited()
@pytest.mark.asyncio
async def test_auto_block_skips_completed_task() -> None:
orch = _make_orch()
client: Any = AsyncMock()
client.get.return_value = _resp(True, {"status": "completed"})
await orch._auto_block_task(client, "tid-2", "stale readiness check")
client.patch.assert_not_awaited()
@pytest.mark.asyncio
async def test_auto_block_proceeds_for_pending_task() -> None:
"""The main existing use case (stuck pending tasks) must be unaffected."""
orch = _make_orch()
client: Any = AsyncMock()
client.get.return_value = _resp(True, {"status": "pending"})
await orch._auto_block_task(client, "tid-3", "needs a project_id")
client.patch.assert_awaited_once()
args, kwargs = client.patch.await_args
assert "tid-3" in args[0]
assert kwargs["json"]["status"] == "blocked"
@pytest.mark.asyncio
async def test_auto_block_proceeds_when_status_precheck_fails() -> None:
"""A GET failure must not swallow the block attempt — fall through."""
orch = _make_orch()
client: Any = AsyncMock()
client.get.side_effect = RuntimeError("network down")
await orch._auto_block_task(client, "tid-4", "some reason")
client.patch.assert_awaited_once()
@pytest.mark.asyncio
async def test_auto_block_logs_nonempty_error_for_blank_exception() -> None:
"""str(TimeoutError()) is '' — the log must still carry a real message."""
assert str(TimeoutError()) == "" # the exact gotcha this guards against
orch = _make_orch()
client: Any = AsyncMock()
client.get.return_value = _resp(True, {"status": "pending"})
client.patch.side_effect = TimeoutError()
with patch("roboco.runtime.orchestrator.logger") as mock_logger:
await orch._auto_block_task(client, "tid-5", "some reason")
mock_logger.error.assert_called_once()
_, kwargs = mock_logger.error.call_args
assert kwargs["error"], "error field must never be blank"
if __name__ == "__main__":
pytest.main([__file__, "-q"])