- is_admin flag is the sole admin authority; remove role-name fallbacks (dependencies, reports, tasks, agent lifecycle, security badge, frontend) - normalize (resource, ACTION) across REST/MCP/catalog; strict admin role/permission parsing (400 on unknown) - gate LLM provider CRUD/status/test on admin:settings; require auth on agent llm-status; quarantine listing requires scenario:automation MANAGE - enforce logical-session revocation/idle in require_api_key_or_jwt - last-admin lockout guards for user/role CRUD; block non-admin from granting is_admin - frontend: flag-only admin, route permission map from the nav registry, write-control gating, 403 dedupe - tests: orthogonal edge coverage (flag-only admin, lockout boundaries, session states, normalization)
144 lines
5.9 KiB
Python
144 lines
5.9 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
|
|
|
|
# Ensure backend/src is importable for src.api / src.core modules
|
|
_src = str(Path(__file__).resolve().parent.parent.parent / "src")
|
|
if _src not in sys.path:
|
|
sys.path.insert(0, _src)
|
|
|
|
|
|
def _make_client(authenticated: bool = True) -> TestClient:
|
|
"""Build a TestClient with the agent status router.
|
|
|
|
The endpoint requires an authenticated user (RBAC-02); tests override
|
|
get_current_user unless the test is specifically asserting the 401 path.
|
|
"""
|
|
from src.api.routes.agent_status import router
|
|
|
|
app = FastAPI()
|
|
app.include_router(router)
|
|
if authenticated:
|
|
from src.dependencies import get_current_user
|
|
|
|
app.dependency_overrides[get_current_user] = lambda: MagicMock(
|
|
username="tester", id="user-1"
|
|
)
|
|
return TestClient(app)
|
|
|
|
|
|
class TestGetLlmStatus:
|
|
"""GET /api/agent/llm-status"""
|
|
|
|
@patch("src.core.utils.llm_health._check_llm_provider_health", AsyncMock(return_value="ok"))
|
|
@patch("src.core.utils.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("src.core.utils.llm_health._check_llm_provider_health", AsyncMock(return_value="unavailable"))
|
|
@patch("src.core.utils.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("src.core.utils.llm_health._check_llm_provider_health", AsyncMock(return_value="timeout"))
|
|
@patch("src.core.utils.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("src.core.utils.llm_health._check_llm_provider_health", AsyncMock(return_value="auth_error"))
|
|
@patch("src.core.utils.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("src.core.utils.llm_health._check_llm_provider_health", AsyncMock(return_value="ok"))
|
|
@patch("src.core.utils.llm_health._llm_status", {"status": "ok", "last_error": ""})
|
|
def test_auth_required(self):
|
|
"""Status endpoint requires authentication (RBAC-02: no anonymous provider probe)."""
|
|
client = _make_client(authenticated=False)
|
|
resp = client.get("/api/agent/llm-status")
|
|
assert resp.status_code == 401
|
|
|
|
def test_check_health_called(self):
|
|
"""_check_llm_provider_health is called on each request."""
|
|
mock_check = AsyncMock(return_value="ok")
|
|
with (
|
|
patch("src.core.utils.llm_health._check_llm_provider_health", mock_check),
|
|
patch("src.core.utils.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("src.core.utils.llm_health._check_llm_provider_health", mock_check),
|
|
patch("src.core.utils.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("src.core.utils.llm_health._check_llm_provider_health", mock_check),
|
|
patch("src.core.utils.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
|