mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
fix(optimal): serialize singleton init, normalize kb_search index_types, surface mentor errors
The OptimalService singleton published the instance before initialize() finished, so a concurrent caller could observe _initialized=False and hit "OptimalService not initialized" during RAG indexing. Build the instance, initialize it, then publish under a lazily-bound asyncio lock so all callers share a fully-initialized singleton. roboco_kb_search forwarded the legacy alias index_types=['docs'], which is not a valid IndexType value (the enum value is 'documentation'), producing a 400 at the route. Normalize the alias in the client before the request is sent and fix the misleading tool docstring. The mentor route let exceptions from mentor.ask escape as a bare 500 that masked the real cause. Catch, log the true upstream error with stack, and surface it in the response detail so failures are diagnosable.
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
"""The mentor route must surface (log) the real upstream error, not mask it.
|
||||
|
||||
Previously an exception raised inside ``mentor.ask`` escaped the route as a
|
||||
bare 500 whose body carried no diagnosable cause — agents saw only a generic
|
||||
mask. The route must now log the true error and return a clean envelope whose
|
||||
``detail`` names the real cause.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from http import HTTPStatus
|
||||
from typing import TYPE_CHECKING
|
||||
from unittest.mock import AsyncMock, patch
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
from fastapi import FastAPI
|
||||
from httpx import ASGITransport, AsyncClient
|
||||
from roboco.api.deps import get_agent_context
|
||||
from roboco.api.routes.optimal import router as optimal_router
|
||||
from roboco.models import AgentRole
|
||||
from roboco.models.permissions import AgentContext
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import AsyncIterator
|
||||
|
||||
|
||||
_HDR = {"X-Agent-ID": str(uuid4()), "X-Agent-Role": "ceo"}
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def optimal_client() -> AsyncIterator[AsyncClient]:
|
||||
app = FastAPI()
|
||||
app.include_router(optimal_router, prefix="/api/optimal")
|
||||
|
||||
async def _override_agent() -> AgentContext:
|
||||
return AgentContext(agent_id=uuid4(), role=AgentRole.CEO, team=None)
|
||||
|
||||
app.dependency_overrides[get_agent_context] = _override_agent
|
||||
transport = ASGITransport(app=app)
|
||||
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
||||
yield client
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mentor_ask_logs_and_surfaces_real_upstream_error(
|
||||
optimal_client: AsyncClient,
|
||||
) -> None:
|
||||
mentor = AsyncMock()
|
||||
mentor._optimal_service = object() # already initialized
|
||||
mentor.ask = AsyncMock(side_effect=RuntimeError("ollama connection refused"))
|
||||
|
||||
with (
|
||||
patch("roboco.api.routes.optimal.get_mentor_service") as mock_mentor,
|
||||
patch("roboco.api.routes.optimal.get_optimal_service") as mock_get,
|
||||
patch("roboco.api.routes.optimal.logger") as mock_logger,
|
||||
):
|
||||
mock_mentor.return_value = mentor
|
||||
mock_get.return_value = AsyncMock()
|
||||
response = await optimal_client.post(
|
||||
"/api/optimal/mentor/ask",
|
||||
json={"question": "What"},
|
||||
headers=_HDR,
|
||||
)
|
||||
|
||||
# The route must not 200 on a failed ask.
|
||||
assert response.status_code in (
|
||||
HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
HTTPStatus.SERVICE_UNAVAILABLE,
|
||||
)
|
||||
# The real cause must be diagnosable in the response body, not masked.
|
||||
assert "ollama connection refused" in response.text
|
||||
|
||||
# The real error must be logged (so it is diagnosable server-side even if
|
||||
# the envelope is later sanitized).
|
||||
logged_text = "".join(
|
||||
str(call.args) + str(call.kwargs)
|
||||
for call in (
|
||||
*mock_logger.error.call_args_list,
|
||||
*mock_logger.exception.call_args_list,
|
||||
)
|
||||
)
|
||||
assert "ollama connection refused" in logged_text
|
||||
Reference in New Issue
Block a user