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

167 lines
5.8 KiB
Python
Raw Normal View History

"""Task complexity threads into `_resolve_agent_route` -> `resolve_for_agent`.
Cost-tiered routing (roboco/services/llm.py) reads a task's
`estimated_complexity` to try a compound ROLE(":"complexity) row before
falling to the plain ROLE row. The orchestrator owns the one indexed Task
lookup and threads the lowercase complexity string through. This is the pure
wiring test (`_resolve_agent_route` -> `resolve_for_agent`); the precedence
logic itself is covered in tests/integration/test_llm_routing.py.
"""
from __future__ import annotations
from typing import Any
from unittest.mock import AsyncMock, MagicMock
import pytest
import roboco.db.base as db_base
import roboco.services.llm as llm_module
from roboco.models.base import Complexity, ModelProvider
from roboco.runtime.orchestrator import AgentOrchestrator
from roboco.services.llm import AgentRoute
# Sentinel route the mocked resolve_for_agent returns — a real AgentRoute
# instance (not a bare string) so `result is _SENTINEL_ROUTE` type-checks
# cleanly against `_resolve_agent_route`'s declared AgentRoute return type.
_SENTINEL_ROUTE = AgentRoute(
provider_id=None,
provider_type=ModelProvider.ANTHROPIC,
base_url=None,
auth_token=None,
model_name="sentinel",
)
class _ScalarResult:
def __init__(self, value: Any) -> None:
self._value = value
def scalar_one_or_none(self) -> Any:
return self._value
class _FakeSession:
"""Minimal async-context-manager session returning a fixed complexity."""
def __init__(self, complexity_value: Any) -> None:
self._complexity_value = complexity_value
def __call__(self) -> _FakeSession:
return self
async def __aenter__(self) -> _FakeSession:
return self
async def __aexit__(self, *exc: object) -> None:
return None
async def execute(self, _stmt: Any) -> _ScalarResult:
return _ScalarResult(self._complexity_value)
class _BoomSession(_FakeSession):
"""A session whose `execute` always raises — models a task-lookup failure
(bad/unresolvable task id) distinct from a genuine DB/session outage."""
async def execute(self, _stmt: Any) -> _ScalarResult:
raise RuntimeError("bad task id")
def _orch() -> AgentOrchestrator:
# __new__ + skip __init__: avoid all constructor I/O — this method is pure
# w.r.t. instance state (it only touches module-level imports + args).
return AgentOrchestrator.__new__(AgentOrchestrator)
def _wire(monkeypatch: pytest.MonkeyPatch, fake_session: Any) -> AsyncMock:
"""Patch get_session_factory + get_model_routing_service; return the
resolve_for_agent mock so the test can assert on its call."""
monkeypatch.setattr(
db_base,
"get_session_factory",
lambda: MagicMock(return_value=fake_session),
)
resolve_mock = AsyncMock(return_value=_SENTINEL_ROUTE)
fake_router = MagicMock(resolve_for_agent=resolve_mock)
monkeypatch.setattr(
llm_module, "get_model_routing_service", lambda _db: fake_router
)
return resolve_mock
@pytest.mark.asyncio
async def test_task_with_high_complexity_threads_lowercase_string(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""A task with estimated_complexity=HIGH resolves the compound
'role:high' row — i.e. resolve_for_agent is called with complexity='high'
(lowercased from the Complexity enum's value)."""
resolve_mock = _wire(monkeypatch, _FakeSession(Complexity.HIGH))
orch = _orch()
result = await orch._resolve_agent_route("be-dev-1", "task-123")
assert result is _SENTINEL_ROUTE
resolve_mock.assert_awaited_once_with("be-dev-1", complexity="high")
@pytest.mark.asyncio
async def test_task_with_low_complexity_threads_lowercase_string(
monkeypatch: pytest.MonkeyPatch,
) -> None:
resolve_mock = _wire(monkeypatch, _FakeSession(Complexity.LOW))
orch = _orch()
await orch._resolve_agent_route("be-dev-1", "task-456")
resolve_mock.assert_awaited_once_with("be-dev-1", complexity="low")
@pytest.mark.asyncio
async def test_taskless_spawn_threads_none_complexity_unchanged(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""A no-task spawn (idle PM bootstrap, Intake/Secretary chats, ...) never
even attempts a task lookup — complexity=None, byte-identical to the
pre-cost-tiering call shape."""
resolve_mock = _wire(monkeypatch, _FakeSession(Complexity.HIGH))
orch = _orch()
result = await orch._resolve_agent_route("be-dev-1", None)
assert result is _SENTINEL_ROUTE
resolve_mock.assert_awaited_once_with("be-dev-1", complexity=None)
@pytest.mark.asyncio
async def test_missing_task_row_degrades_to_none_complexity_silently(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""scalar_one_or_none() returning None (task not found / deleted) is not
an error — complexity falls back to None and routing still proceeds
through the router (not the hardcoded legacy path)."""
resolve_mock = _wire(monkeypatch, _FakeSession(None))
orch = _orch()
result = await orch._resolve_agent_route("be-dev-1", "ghost-task-id")
assert result is _SENTINEL_ROUTE
resolve_mock.assert_awaited_once_with("be-dev-1", complexity=None)
@pytest.mark.asyncio
async def test_task_lookup_failure_degrades_silently_not_to_full_legacy_path(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""A task-lookup-specific failure (bad id, transient query error) must
NOT escalate to the full DB-failure downgrade (hardcoded ROLE_MODEL_MAP,
bypassing model_assignments entirely) — only the complexity lookup is
skipped; AGENT_SLUG/ROLE/GLOBAL resolution still runs via the router."""
resolve_mock = _wire(monkeypatch, _BoomSession(None))
orch = _orch()
result = await orch._resolve_agent_route("be-dev-1", "bad-task-id")
assert result is _SENTINEL_ROUTE
resolve_mock.assert_awaited_once_with("be-dev-1", complexity=None)