- 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
139 lines
5.7 KiB
Python
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
|