File size: 4,504 Bytes
7c6ffa6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f415181
 
7c6ffa6
f415181
 
 
 
 
 
 
 
 
 
 
 
 
7c6ffa6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
"""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)