| """Tests for AI provider fallback tracking and is_fallback signal.""" |
|
|
| from __future__ import annotations |
|
|
| import os |
| import sys |
| from pathlib import Path |
| from unittest.mock import MagicMock, patch |
|
|
| BACKEND_DIR = Path(__file__).resolve().parents[1] |
| if str(BACKEND_DIR) not in sys.path: |
| sys.path.insert(0, str(BACKEND_DIR)) |
|
|
| os.environ.setdefault("AI_PROVIDER", "mock") |
| os.environ.setdefault("AUTH_ENABLED", "false") |
|
|
| from app.core.config import get_settings |
|
|
| get_settings.cache_clear() |
|
|
| from app.services.ai_provider import ( |
| MockAIProvider, |
| get_ai_provider, |
| ) |
|
|
|
|
| def test_mock_provider_not_fallback_when_configured_as_mock(): |
| os.environ["AI_PROVIDER"] = "mock" |
| get_settings.cache_clear() |
| provider = get_ai_provider() |
| assert isinstance(provider, MockAIProvider) |
| assert provider.is_fallback is False |
| assert provider.fallback_reason is None |
|
|
|
|
| def test_mock_provider_is_fallback_when_openrouter_key_missing(): |
| """When AI_PROVIDER=openrouter but key is absent, get_ai_provider must |
| return a MockAIProvider with is_fallback=True. |
| |
| We mock get_settings so the .env file on disk (which may have a real key) |
| does not interfere with the test. |
| """ |
| mock_settings = MagicMock() |
| mock_settings.ai_provider = "openrouter" |
| mock_settings.openrouter_api_key = None |
|
|
| with patch("app.services.ai_provider.get_settings", return_value=mock_settings): |
| provider = get_ai_provider() |
|
|
| assert isinstance(provider, MockAIProvider) |
| assert provider.is_fallback is True |
| assert "not configured" in (provider.fallback_reason or "").lower() |
|
|
|
|
| def test_mock_provider_model_name(): |
| os.environ["AI_PROVIDER"] = "mock" |
| get_settings.cache_clear() |
| provider = get_ai_provider() |
| assert "mock" in provider.model_name.lower() |
|
|
|
|
| def test_health_ai_endpoint_requires_admin(client): |
| """Provider diagnostics must not be exposed to anonymous callers.""" |
| resp = client.get("/health/ai") |
| assert resp.status_code == 403 |
|
|
|
|
| def test_health_ai_endpoint_returns_provider_status_for_admin(client): |
| """The admin-only diagnostic still reports the configured provider.""" |
| from app.core.admin_auth import require_admin |
|
|
| client.app.dependency_overrides[require_admin] = lambda: None |
| try: |
| resp = client.get("/health/ai") |
| finally: |
| client.app.dependency_overrides.pop(require_admin, None) |
|
|
| assert resp.status_code == 200 |
| data = resp.json() |
| assert data["status"] in ("ready", "demo", "misconfigured") |
| assert "provider" in data |
| assert "can_generate" in data |
|
|
|
|
| def test_ask_response_has_is_fallback(client): |
| resp = client.post("/ask", json={"question": "What is photosynthesis?"}) |
| assert resp.status_code == 200 |
| data = resp.json() |
| assert "is_fallback" in data |
| assert isinstance(data["is_fallback"], bool) |
| assert "model_used" in data |
|
|
|
|
| def test_studio_notes_response_has_is_fallback(client): |
| resp = client.post( |
| "/generate/notes", |
| json={"topic": "Photosynthesis", "language_preference": "English"}, |
| ) |
| assert resp.status_code == 200 |
| data = resp.json() |
| assert "is_fallback" in data |
| assert isinstance(data["is_fallback"], bool) |
| assert "model_used" in data |
|
|
|
|
| def test_studio_exam_answer_response_has_is_fallback(client): |
| resp = client.post( |
| "/generate/exam-answer", |
| json={"topic": "Photosynthesis", "language_preference": "English"}, |
| ) |
| assert resp.status_code == 200 |
| data = resp.json() |
| assert "is_fallback" in data |
| assert isinstance(data["is_fallback"], bool) |
|
|
|
|
| def test_mock_provider_generates_without_fallback(): |
| os.environ["AI_PROVIDER"] = "mock" |
| get_settings.cache_clear() |
| provider = get_ai_provider() |
| output = provider.generate_simple_explanation( |
| context="Photosynthesis is the process by which plants make food using sunlight.", |
| language="English", |
| ) |
| assert provider.is_fallback is False |
| assert "simple_meaning" in output |
|
|
|
|
| def test_unknown_provider_falls_back_to_mock(): |
| mock_settings = MagicMock() |
| mock_settings.ai_provider = "nonexistent-provider" |
| mock_settings.sarvam_api_key = None |
| mock_settings.openrouter_api_key = None |
| mock_settings.gemini_api_key = None |
| mock_settings.environment = "development" |
| mock_settings.ai_fallback_to_mock = True |
|
|
| with patch("app.services.ai_provider.get_settings", return_value=mock_settings): |
| provider = get_ai_provider() |
| assert isinstance(provider, MockAIProvider) |
|
|