Files
ss-tools/backend/tests/api/test_agent_status_routes.py
busya e291ba757f fix(agent): full-catalog dashboard search and working LLM retry
- search_dashboards: call /api/dashboards with page_context=other and
  page_size=100 so the profile 'My Dashboards Only' filter can no longer
  hide the whole catalog; parse available_total/effective_profile_filter
  and report hidden-by-filter instead of a false 'no dashboards' answer
- prefetch_dashboards: same full-catalog context; fix dead code where
  data=resp.json() sat after return '' inside the error branch, making
  every 200 response raise NameError and the prefetch always return ''
- llm-status: ?force=1 bypasses the 30s health cache so the 'Retry now'
  button performs a fresh probe instead of re-reading the stale status;
  frontend keeps a single retry interval (previously stacked intervals
  decayed the countdown faster than 1/s and fired duplicate probes)
- tests: agent tool/prefetch, backend route bypass + force param,
  frontend retry/force coverage
2026-08-19 19:21:00 +03:00

139 lines
5.7 KiB
Python

# #region Test.Api.AgentStatusRoutes [C:2] [TYPE Module] [SEMANTICS test,agent,llm,status,health]
# @BRIEF Unit tests for Agent LLM provider health status endpoint.
# @RELATION BINDS_TO -> [Api.Agent.Status]
import os
os.environ.setdefault("DATABASE_URL", "sqlite:///:memory:")
os.environ.setdefault("AUTH_DATABASE_URL", "sqlite:///:memory:")
os.environ.setdefault("SECRET_KEY", "test-secret-key-for-tests")
os.environ.setdefault("DEV_MODE", "true")
import sys
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
# Add shared/src to path for ss_tools.shared._llm_health
_src = str(Path(__file__).resolve().parent.parent.parent / "src")
if _src not in sys.path:
sys.path.insert(0, _src)
_shared_src = str(Path(__file__).resolve().parent.parent.parent.parent / "shared" / "src")
if _shared_src not in sys.path:
sys.path.insert(0, _shared_src)
def _make_client() -> TestClient:
"""Build a TestClient with the agent status router."""
from src.api.routes.agent_status import router
app = FastAPI()
app.include_router(router)
return TestClient(app)
class TestGetLlmStatus:
"""GET /api/agent/llm-status"""
@patch("ss_tools.shared._llm_health._check_llm_provider_health", AsyncMock(return_value="ok"))
@patch("ss_tools.shared._llm_health._llm_status", {"status": "ok", "last_error": ""})
def test_status_ok(self):
"""Returns ok when LLM provider is healthy."""
client = _make_client()
resp = client.get("/api/agent/llm-status")
assert resp.status_code == 200
data = resp.json()
assert data["status"] == "ok"
assert data["last_error"] == ""
assert data["retry_after_s"] == 0
@patch("ss_tools.shared._llm_health._check_llm_provider_health", AsyncMock(return_value="unavailable"))
@patch("ss_tools.shared._llm_health._llm_status", {"status": "unavailable", "last_error": "Provider unreachable"})
def test_status_unavailable(self):
"""Returns unavailable with retry_after_s > 0."""
client = _make_client()
resp = client.get("/api/agent/llm-status")
assert resp.status_code == 200
data = resp.json()
assert data["status"] == "unavailable"
assert data["last_error"] == "Provider unreachable"
assert data["retry_after_s"] == 30
@patch("ss_tools.shared._llm_health._check_llm_provider_health", AsyncMock(return_value="timeout"))
@patch("ss_tools.shared._llm_health._llm_status", {"status": "timeout", "last_error": "Request timed out"})
def test_status_timeout(self):
"""Returns timeout status."""
client = _make_client()
resp = client.get("/api/agent/llm-status")
assert resp.status_code == 200
assert resp.json()["status"] == "timeout"
@patch("ss_tools.shared._llm_health._check_llm_provider_health", AsyncMock(return_value="auth_error"))
@patch("ss_tools.shared._llm_health._llm_status", {"status": "auth_error", "last_error": "Invalid API key"})
def test_status_auth_error(self):
"""Returns auth_error status."""
client = _make_client()
resp = client.get("/api/agent/llm-status")
assert resp.status_code == 200
assert resp.json()["status"] == "auth_error"
assert resp.json()["last_error"] == "Invalid API key"
@patch("ss_tools.shared._llm_health._check_llm_provider_health", AsyncMock(return_value="ok"))
@patch("ss_tools.shared._llm_health._llm_status", {"status": "ok", "last_error": ""})
def test_no_auth_required(self):
"""Status endpoint does not require authentication."""
client = _make_client()
# No auth token — should still work since no Depends on get_current_user
resp = client.get("/api/agent/llm-status")
assert resp.status_code == 200
def test_check_health_called(self):
"""_check_llm_provider_health is called on each request."""
mock_check = AsyncMock(return_value="ok")
with (
patch("ss_tools.shared._llm_health._check_llm_provider_health", mock_check),
patch("ss_tools.shared._llm_health._llm_status", {"status": "ok", "last_error": ""}),
):
client = _make_client()
resp = client.get("/api/agent/llm-status")
assert resp.status_code == 200
mock_check.assert_called_once()
def test_force_param_bypasses_cache(self):
"""?force=true must reach _check_llm_provider_health(force=True) so the
"Retry now" button gets a fresh probe instead of the 30s cached status."""
mock_check = AsyncMock(return_value="unavailable")
with (
patch("ss_tools.shared._llm_health._check_llm_provider_health", mock_check),
patch("ss_tools.shared._llm_health._llm_status", {"status": "unavailable", "last_error": "Provider unreachable"}),
):
client = _make_client()
resp = client.get("/api/agent/llm-status?force=true")
assert resp.status_code == 200
assert resp.json()["status"] == "unavailable"
mock_check.assert_called_once_with(force=True)
def test_force_false_default_passes_no_force(self):
"""Without the force param the health check is called with force=False (cached)."""
mock_check = AsyncMock(return_value="ok")
with (
patch("ss_tools.shared._llm_health._check_llm_provider_health", mock_check),
patch("ss_tools.shared._llm_health._llm_status", {"status": "ok", "last_error": ""}),
):
client = _make_client()
resp = client.get("/api/agent/llm-status")
assert resp.status_code == 200
mock_check.assert_called_once_with(force=False)
# #endregion Test.Api.AgentStatusRoutes