Spaces:
Build error
Build error
File size: 15,791 Bytes
02197c5 11f019c 02197c5 11f019c 02197c5 af0c4ba 02197c5 11f019c af0c4ba 02197c5 af0c4ba 02197c5 af0c4ba 02197c5 af0c4ba 02197c5 af0c4ba 02197c5 af0c4ba 02197c5 af0c4ba 11f019c 02197c5 af0c4ba 02197c5 af0c4ba 02197c5 af0c4ba 02197c5 af0c4ba 02197c5 11f019c 02197c5 11f019c 02197c5 11f019c 02197c5 | 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 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 | """
Tests des providers Google AI et du registre de modèles.
Aucun appel réseau réel — tous les clients SDK sont mockés.
"""
# 1. stdlib
import json
from datetime import datetime, timezone
from unittest.mock import MagicMock, patch
# 2. third-party
import pytest
from pydantic import ValidationError
# 3. local
from app.schemas.model_config import ModelConfig, ModelInfo, ProviderType
from app.services.ai.base import is_vision_model
from app.services.ai.model_registry import build_model_config, list_all_models
from app.services.ai.provider_google_ai import GoogleAIProvider
from app.services.ai.provider_vertex_sa import VertexServiceAccountProvider
# ---------------------------------------------------------------------------
# Données de test partagées
# ---------------------------------------------------------------------------
FAKE_SA_JSON = {
"type": "service_account",
"project_id": "test-project-123",
"private_key_id": "key-abc",
"private_key": "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEA\n-----END RSA PRIVATE KEY-----\n",
"client_email": "test-sa@test-project-123.iam.gserviceaccount.com",
"client_id": "123456789",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
}
def _make_mock_model(
name: str = "models/gemini-1.5-pro",
display_name: str = "Gemini 1.5 Pro",
methods: list[str] | None = None,
input_token_limit: int = 1_000_000,
output_token_limit: int = 8192,
) -> MagicMock:
"""Construit un objet modèle factice imitant google.genai.types.Model."""
m = MagicMock()
m.name = name
m.display_name = display_name
m.supported_generation_methods = methods if methods is not None else ["generateContent"]
m.input_token_limit = input_token_limit
m.output_token_limit = output_token_limit
return m
# ---------------------------------------------------------------------------
# Tests — ModelInfo (schéma)
# ---------------------------------------------------------------------------
def test_model_info_valid():
info = ModelInfo(
model_id="models/gemini-1.5-pro",
display_name="Gemini 1.5 Pro",
provider=ProviderType.GOOGLE_AI_STUDIO,
supports_vision=True,
input_token_limit=1_000_000,
output_token_limit=8192,
)
assert info.model_id == "models/gemini-1.5-pro"
assert info.supports_vision is True
def test_model_info_is_frozen():
info = ModelInfo(
model_id="models/gemini-1.5-pro",
display_name="Gemini 1.5 Pro",
provider=ProviderType.GOOGLE_AI_STUDIO,
supports_vision=True,
)
with pytest.raises((TypeError, ValidationError)):
info.model_id = "changed" # type: ignore[misc]
def test_model_info_optional_token_limits():
info = ModelInfo(
model_id="models/gemini-2.0-flash",
display_name="Gemini 2.0 Flash",
provider=ProviderType.VERTEX_SERVICE_ACCOUNT,
supports_vision=True,
)
assert info.input_token_limit is None
assert info.output_token_limit is None
def test_model_info_all_provider_types():
for ptype in ProviderType:
info = ModelInfo(
model_id=f"models/test-{ptype.value}",
display_name="Test",
provider=ptype,
supports_vision=False,
)
assert info.provider == ptype
# ---------------------------------------------------------------------------
# Tests — ModelConfig (schéma)
# ---------------------------------------------------------------------------
def test_model_config_valid():
cfg = ModelConfig(
corpus_id="corpus-001",
selected_model_id="models/gemini-1.5-pro",
selected_model_display_name="Gemini 1.5 Pro",
provider=ProviderType.GOOGLE_AI_STUDIO,
supports_vision=True,
last_fetched_at=datetime(2026, 3, 17, tzinfo=timezone.utc),
)
assert cfg.corpus_id == "corpus-001"
assert cfg.supports_vision is True
def test_model_config_missing_required_field():
with pytest.raises(ValidationError):
ModelConfig.model_validate({"corpus_id": "x"})
# ---------------------------------------------------------------------------
# Tests — is_vision_model helper
# ---------------------------------------------------------------------------
def test_is_vision_model_gemini():
m = MagicMock()
m.name = "models/gemini-1.5-pro"
m.display_name = "Gemini 1.5 Pro"
assert is_vision_model(m) is True
def test_is_vision_model_vision_in_name():
m = MagicMock()
m.name = "models/some-vision-model"
m.display_name = "Some Model"
assert is_vision_model(m) is True
def test_is_vision_model_vision_in_display():
m = MagicMock()
m.name = "models/some-model"
m.display_name = "Some Vision Model"
assert is_vision_model(m) is True
def test_is_vision_model_text_only():
m = MagicMock()
m.name = "models/text-embedding-004"
m.display_name = "Text Embedding"
assert is_vision_model(m) is False
# ---------------------------------------------------------------------------
# Tests — GoogleAIProvider
# ---------------------------------------------------------------------------
def test_google_ai_provider_not_configured(monkeypatch):
monkeypatch.delenv("GOOGLE_AI_STUDIO_API_KEY", raising=False)
provider = GoogleAIProvider()
assert provider.is_configured() is False
def test_google_ai_provider_configured(monkeypatch):
monkeypatch.setenv("GOOGLE_AI_STUDIO_API_KEY", "fake-key")
provider = GoogleAIProvider()
assert provider.is_configured() is True
def test_google_ai_provider_type():
assert GoogleAIProvider().provider_type == ProviderType.GOOGLE_AI_STUDIO
def test_google_ai_provider_list_models_not_configured(monkeypatch):
monkeypatch.delenv("GOOGLE_AI_STUDIO_API_KEY", raising=False)
with pytest.raises(RuntimeError, match="GOOGLE_AI_STUDIO_API_KEY"):
GoogleAIProvider().list_models()
def test_google_ai_provider_list_models_success(monkeypatch):
monkeypatch.setenv("GOOGLE_AI_STUDIO_API_KEY", "fake-key")
mock_model = _make_mock_model()
with patch("app.services.ai.provider_google_ai.genai.Client") as MockClient:
MockClient.return_value.models.list.return_value = [mock_model]
models = GoogleAIProvider().list_models()
assert len(models) == 1
assert models[0].model_id == "models/gemini-1.5-pro"
assert models[0].provider == ProviderType.GOOGLE_AI_STUDIO
assert models[0].supports_vision is True
MockClient.assert_called_once_with(api_key="fake-key")
def test_google_ai_provider_filters_non_generate_content(monkeypatch):
monkeypatch.setenv("GOOGLE_AI_STUDIO_API_KEY", "fake-key")
embedding = _make_mock_model(
name="models/text-embedding-004",
display_name="Text Embedding",
methods=["embedContent"],
)
gemini = _make_mock_model()
with patch("app.services.ai.provider_google_ai.genai.Client") as MockClient:
MockClient.return_value.models.list.return_value = [embedding, gemini]
models = GoogleAIProvider().list_models()
assert len(models) == 1
assert models[0].model_id == "models/gemini-1.5-pro"
def test_google_ai_provider_empty_list(monkeypatch):
monkeypatch.setenv("GOOGLE_AI_STUDIO_API_KEY", "fake-key")
with patch("app.services.ai.provider_google_ai.genai.Client") as MockClient:
MockClient.return_value.models.list.return_value = []
models = GoogleAIProvider().list_models()
assert models == []
# ---------------------------------------------------------------------------
# Tests — VertexServiceAccountProvider
# ---------------------------------------------------------------------------
def test_vertex_sa_provider_not_configured(monkeypatch):
monkeypatch.delenv("VERTEX_SERVICE_ACCOUNT_JSON", raising=False)
assert VertexServiceAccountProvider().is_configured() is False
def test_vertex_sa_provider_configured(monkeypatch):
monkeypatch.setenv("VERTEX_SERVICE_ACCOUNT_JSON", json.dumps(FAKE_SA_JSON))
assert VertexServiceAccountProvider().is_configured() is True
def test_vertex_sa_provider_type():
assert VertexServiceAccountProvider().provider_type == ProviderType.VERTEX_SERVICE_ACCOUNT
def test_vertex_sa_provider_list_models_not_configured(monkeypatch):
monkeypatch.delenv("VERTEX_SERVICE_ACCOUNT_JSON", raising=False)
with pytest.raises(RuntimeError, match="VERTEX_SERVICE_ACCOUNT_JSON"):
VertexServiceAccountProvider().list_models()
def test_vertex_sa_provider_invalid_json(monkeypatch):
monkeypatch.setenv("VERTEX_SERVICE_ACCOUNT_JSON", "not-valid-json{{{")
with pytest.raises(ValueError, match="JSON invalide"):
VertexServiceAccountProvider().list_models()
def test_vertex_sa_provider_missing_project_id(monkeypatch):
sa_no_project = {k: v for k, v in FAKE_SA_JSON.items() if k != "project_id"}
monkeypatch.setenv("VERTEX_SERVICE_ACCOUNT_JSON", json.dumps(sa_no_project))
with pytest.raises(ValueError, match="project_id"):
VertexServiceAccountProvider().list_models()
def test_vertex_sa_provider_list_models_success(monkeypatch):
monkeypatch.setenv("VERTEX_SERVICE_ACCOUNT_JSON", json.dumps(FAKE_SA_JSON))
mock_model = _make_mock_model(
name="models/gemini-1.5-pro-002",
display_name="Gemini 1.5 Pro 002",
)
mock_credentials = MagicMock()
with patch(
"app.services.ai.provider_vertex_sa.service_account.Credentials.from_service_account_info",
return_value=mock_credentials,
) as mock_creds_factory:
with patch("app.services.ai.provider_vertex_sa.genai.Client") as MockClient:
MockClient.return_value.models.list.return_value = [mock_model]
models = VertexServiceAccountProvider().list_models()
assert len(models) == 1
assert models[0].model_id == "models/gemini-1.5-pro-002"
assert models[0].provider == ProviderType.VERTEX_SERVICE_ACCOUNT
mock_creds_factory.assert_called_once_with(
FAKE_SA_JSON,
scopes=["https://www.googleapis.com/auth/cloud-platform"],
)
MockClient.assert_called_once_with(
vertexai=True,
project="test-project-123",
location="us-central1",
credentials=mock_credentials,
)
def test_vertex_sa_provider_filters_non_generate_content(monkeypatch):
monkeypatch.setenv("VERTEX_SERVICE_ACCOUNT_JSON", json.dumps(FAKE_SA_JSON))
embedding = _make_mock_model(
name="models/textembedding-gecko",
display_name="Text Embedding Gecko",
methods=["embedContent"],
)
with patch(
"app.services.ai.provider_vertex_sa.service_account.Credentials.from_service_account_info",
return_value=MagicMock(),
):
with patch("app.services.ai.provider_vertex_sa.genai.Client") as MockClient:
MockClient.return_value.models.list.return_value = [embedding]
models = VertexServiceAccountProvider().list_models()
assert models == []
# ---------------------------------------------------------------------------
# Tests — model_registry.list_all_models
# ---------------------------------------------------------------------------
def test_list_all_models_no_providers_configured(monkeypatch):
monkeypatch.delenv("GOOGLE_AI_STUDIO_API_KEY", raising=False)
monkeypatch.delenv("VERTEX_SERVICE_ACCOUNT_JSON", raising=False)
result = list_all_models()
assert result == []
def test_list_all_models_one_provider(monkeypatch):
monkeypatch.setenv("GOOGLE_AI_STUDIO_API_KEY", "fake-key")
monkeypatch.delenv("VERTEX_SERVICE_ACCOUNT_JSON", raising=False)
mock_model = _make_mock_model()
with patch("app.services.ai.provider_google_ai.genai.Client") as MockClient:
MockClient.return_value.models.list.return_value = [mock_model]
result = list_all_models()
assert len(result) == 1
assert result[0].provider == ProviderType.GOOGLE_AI_STUDIO
def test_list_all_models_aggregates_multiple_providers(monkeypatch):
"""Deux providers configurés → les deux listes sont agrégées.
On utilise Google AI Studio + Vertex Service Account pour tester l'agrégation.
"""
monkeypatch.setenv("GOOGLE_AI_STUDIO_API_KEY", "fake-key-ai")
monkeypatch.setenv("VERTEX_SERVICE_ACCOUNT_JSON", "{}") # déclenche is_configured()
models_ai = [ModelInfo(
model_id="models/gemini-1.5-pro",
display_name="Gemini 1.5 Pro",
provider=ProviderType.GOOGLE_AI_STUDIO,
supports_vision=True,
)]
models_sa = [ModelInfo(
model_id="models/gemini-2.0-flash",
display_name="Gemini 2.0 Flash",
provider=ProviderType.VERTEX_SERVICE_ACCOUNT,
supports_vision=True,
)]
with patch.object(GoogleAIProvider, "list_models", return_value=models_ai):
with patch.object(VertexServiceAccountProvider, "list_models", return_value=models_sa):
result = list_all_models()
assert len(result) == 2
providers = {m.provider for m in result}
assert ProviderType.GOOGLE_AI_STUDIO in providers
assert ProviderType.VERTEX_SERVICE_ACCOUNT in providers
def test_list_all_models_failing_provider_is_skipped(monkeypatch):
"""Un provider configuré qui échoue est ignoré ; l'autre est retourné."""
monkeypatch.setenv("GOOGLE_AI_STUDIO_API_KEY", "bad-key")
monkeypatch.setenv("VERTEX_SERVICE_ACCOUNT_JSON", "{}")
models_sa = [ModelInfo(
model_id="models/gemini-2.0-flash",
display_name="Gemini 2.0 Flash",
provider=ProviderType.VERTEX_SERVICE_ACCOUNT,
supports_vision=True,
)]
with patch.object(GoogleAIProvider, "list_models", side_effect=Exception("API key invalid")):
with patch.object(VertexServiceAccountProvider, "list_models", return_value=models_sa):
result = list_all_models()
assert len(result) == 1
assert result[0].provider == ProviderType.VERTEX_SERVICE_ACCOUNT
# ---------------------------------------------------------------------------
# Tests — model_registry.build_model_config
# ---------------------------------------------------------------------------
def test_build_model_config_valid(monkeypatch):
monkeypatch.setenv("GOOGLE_AI_STUDIO_API_KEY", "fake-key")
monkeypatch.delenv("VERTEX_SERVICE_ACCOUNT_JSON", raising=False)
mock_model = _make_mock_model()
with patch("app.services.ai.provider_google_ai.genai.Client") as MockClient:
MockClient.return_value.models.list.return_value = [mock_model]
cfg = build_model_config("corpus-001", "models/gemini-1.5-pro")
assert cfg.corpus_id == "corpus-001"
assert cfg.selected_model_id == "models/gemini-1.5-pro"
assert cfg.selected_model_display_name == "Gemini 1.5 Pro"
assert cfg.provider == ProviderType.GOOGLE_AI_STUDIO
assert cfg.supports_vision is True
def test_build_model_config_unknown_model(monkeypatch):
monkeypatch.setenv("GOOGLE_AI_STUDIO_API_KEY", "fake-key")
monkeypatch.delenv("VERTEX_SERVICE_ACCOUNT_JSON", raising=False)
mock_model = _make_mock_model()
with patch("app.services.ai.provider_google_ai.genai.Client") as MockClient:
MockClient.return_value.models.list.return_value = [mock_model]
with pytest.raises(ValueError, match="non disponible"):
build_model_config("corpus-001", "models/nonexistent-model")
def test_build_model_config_no_providers(monkeypatch):
monkeypatch.delenv("GOOGLE_AI_STUDIO_API_KEY", raising=False)
monkeypatch.delenv("VERTEX_SERVICE_ACCOUNT_JSON", raising=False)
with pytest.raises(ValueError, match="non disponible"):
build_model_config("corpus-001", "models/gemini-1.5-pro")
|