162 lines
6.3 KiB
Python
162 lines
6.3 KiB
Python
"""Tests for model validation, suffix matching, and model discovery refresh."""
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import threading
|
||
from pathlib import Path
|
||
from unittest.mock import MagicMock, patch
|
||
|
||
import pytest
|
||
|
||
pytest.importorskip("customtkinter")
|
||
|
||
from antigravity_provider.router.action_handler import ActionExecutor, do_set_model
|
||
from antigravity_provider.router.model_discovery_service import ModelDiscoveryService
|
||
from antigravity_provider.router.router_config import (
|
||
RolePolicy,
|
||
RouterConfig,
|
||
RouterProfileConfig,
|
||
save_router_config,
|
||
)
|
||
from antigravity_provider.router.ui.model_catalog import CachedModels, refresh_models_async
|
||
|
||
|
||
@pytest.fixture
|
||
def isolated_hub(tmp_path, monkeypatch):
|
||
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
||
|
||
# Reset ModelDiscoveryService singleton
|
||
ModelDiscoveryService._instance = None
|
||
|
||
# Setup a router configuration
|
||
cfg = RouterConfig(
|
||
profiles={
|
||
"ag-orch-1": RouterProfileConfig(
|
||
profile_id="ag-orch-1",
|
||
provider="antigravity",
|
||
enabled=True,
|
||
preferred_models=["google-antigravity/gemini-2.5-pro"],
|
||
),
|
||
"codex-1": RouterProfileConfig(
|
||
profile_id="codex-1",
|
||
provider="openai-codex",
|
||
enabled=True,
|
||
preferred_models=["gpt-4o"],
|
||
),
|
||
},
|
||
roles={
|
||
"manager": RolePolicy(
|
||
role_name="manager",
|
||
preferred_chain=["ag-orch-1"],
|
||
),
|
||
},
|
||
default_role="manager",
|
||
)
|
||
save_router_config(cfg)
|
||
return tmp_path
|
||
|
||
|
||
def test_nonexistent_model_rejected_with_populated_cache(isolated_hub):
|
||
"""Тест 1: Несуществующая модель отклоняется при наполненном кэше."""
|
||
service = ModelDiscoveryService.get()
|
||
with service._cache_lock:
|
||
service._cache["antigravity"] = {
|
||
"models": ["gemini-2.5-pro", "gemini-2.5-flash", "gemini-3.7-flash-high"],
|
||
"discovered_at": 1000.0,
|
||
}
|
||
|
||
ok, msg = do_set_model("ag-orch-1", "completely-fake-model-xyz")
|
||
assert ok is False
|
||
assert "отсутствует в списке обнаруженных моделей провайдера 'antigravity'" in msg
|
||
|
||
|
||
def test_unknown_model_rejected_with_empty_cache(isolated_hub):
|
||
"""Тест 2: При пустом кэше неизвестная модель отклоняется с внятной ошибкой."""
|
||
service = ModelDiscoveryService.get()
|
||
with service._cache_lock:
|
||
service._cache.clear()
|
||
|
||
with patch.object(service, "discover_models_sync", return_value=None):
|
||
ok, msg = do_set_model("ag-orch-1", "unknown-unregistered-model")
|
||
assert ok is False
|
||
assert msg == "Кэш моделей для провайдера 'antigravity' пуст, а модель 'unknown-unregistered-model' не найдена в списке известных моделей."
|
||
|
||
|
||
def test_base_name_without_effort_suffix_accepted(isolated_hub):
|
||
"""Тест 3: Базовое имя без суффикса усилия (например gemini-3.7-flash) принимается, если в кэше есть gemini-3.7-flash-high."""
|
||
service = ModelDiscoveryService.get()
|
||
with service._cache_lock:
|
||
service._cache["antigravity"] = {
|
||
"models": ["gemini-3.7-flash-high", "gemini-3.7-flash-medium"],
|
||
"discovered_at": 1000.0,
|
||
}
|
||
|
||
ok, msg = do_set_model("ag-orch-1", "gemini-3.7-flash")
|
||
assert ok is True
|
||
assert "успешно сохранена" in msg
|
||
|
||
|
||
def test_canonical_model_accepted_even_with_empty_cache(isolated_hub):
|
||
"""Каноническая модель из ModelRegistry принимается даже при пустом кэше discovery."""
|
||
service = ModelDiscoveryService.get()
|
||
with service._cache_lock:
|
||
service._cache.clear()
|
||
|
||
with patch.object(service, "discover_models_sync", return_value=None):
|
||
ok, msg = do_set_model("ag-orch-1", "gemini-2.5-pro")
|
||
assert ok is True
|
||
assert "успешно сохранена" in msg
|
||
|
||
|
||
def test_action_executor_refresh_models(isolated_hub):
|
||
"""ActionExecutor.execute handles 'refresh_models' action."""
|
||
service = ModelDiscoveryService.get()
|
||
with patch.object(service, "discover_models_sync", return_value=["test-model-1"]):
|
||
res = ActionExecutor.execute("refresh_models", {"provider": "antigravity"})
|
||
assert res.get("ok") is True
|
||
assert res.get("data") == ["test-model-1"]
|
||
|
||
with patch.object(service, "refresh_all_async") as mock_refresh_all:
|
||
res = ActionExecutor.execute("refresh_models", {})
|
||
assert res.get("ok") is True
|
||
mock_refresh_all.assert_called_once()
|
||
|
||
|
||
def test_model_discovery_cache_retained_on_timeout_and_error(isolated_hub):
|
||
"""Cache is not overwritten or cleared when model discovery probe times out or raises error."""
|
||
service = ModelDiscoveryService.get()
|
||
with service._cache_lock:
|
||
service._cache["antigravity"] = {
|
||
"models": ["existing-gemini-model"],
|
||
"discovered_at": 1000.0,
|
||
}
|
||
|
||
# Simulate error in _probe_provider
|
||
with patch.object(service, "_probe_provider", side_effect=RuntimeError("Network failure")):
|
||
res = service.discover_models_sync("antigravity", timeout=1.0)
|
||
assert res == ["existing-gemini-model"]
|
||
assert service.get_models("antigravity") == ["existing-gemini-model"]
|
||
|
||
|
||
def test_ui_model_catalog_refresh_models_async(isolated_hub):
|
||
"""model_catalog.refresh_models_async dispatches and completes via service."""
|
||
service = ModelDiscoveryService.get()
|
||
with service._cache_lock:
|
||
service._cache["antigravity"] = {
|
||
"models": ["gemini-2.5-pro"],
|
||
"discovered_at": 1000.0,
|
||
}
|
||
|
||
completed: list[CachedModels] = []
|
||
done_evt = threading.Event()
|
||
|
||
def _on_done(cm: CachedModels) -> None:
|
||
completed.append(cm)
|
||
done_evt.set()
|
||
|
||
with patch.object(service, "_probe_provider", return_value=["gemini-2.5-pro", "gemini-2.5-flash"]):
|
||
ok = refresh_models_async("antigravity", _on_done)
|
||
assert ok is True
|
||
assert done_evt.wait(timeout=3.0) is True
|
||
assert len(completed) == 1
|
||
assert "gemini-2.5-pro" in completed[0].models
|