184 lines
7.3 KiB
Python
184 lines
7.3 KiB
Python
"""Тесты доступности уровней AI (`services.ai_levels.detect_ai_levels`).
|
||
|
||
Детект без GPU → `max` недоступен с причиной; `medium` доступен или
|
||
недоступен по RAM/моделям; ровно три уровня в ответе.
|
||
Реальное железо/файлы моделей недетерминированы на машине разработчика —
|
||
`HW_*` подставляются через `monkeypatch.setenv` + `get_settings.cache_clear()`
|
||
(см. `test_admin_api.py::test_upload_user_avatar_by_admin` за образцом),
|
||
факт «модель скачана» — через monkeypatch `services.ai_levels._model_downloaded`.
|
||
"""
|
||
|
||
from collections.abc import Iterator
|
||
|
||
import pytest
|
||
|
||
from core.config import Settings, get_settings
|
||
from core.plugins.config import ChatConfig, InstanceConfig, SummarizerConfig, TranscriberConfig
|
||
from services import ai_levels
|
||
from services.ai_levels import detect_ai_levels
|
||
|
||
|
||
def _cfg() -> InstanceConfig:
|
||
return InstanceConfig(
|
||
transcriber=TranscriberConfig(), summarizer=SummarizerConfig(), chat=ChatConfig()
|
||
)
|
||
|
||
|
||
@pytest.fixture(autouse=True)
|
||
def _clear_hw_env(monkeypatch: pytest.MonkeyPatch) -> Iterator[None]:
|
||
"""Гарантировать «чистое» железо (без HW_*) на старте каждого теста и сброс кэша
|
||
`get_settings` после — тесты этого модуля не должны зависеть друг от друга
|
||
или от `.env` окружения разработчика."""
|
||
for var in ("HW_CPUS", "HW_RAM_MB", "HW_GPU_NAME", "HW_VRAM_MB"):
|
||
monkeypatch.delenv(var, raising=False)
|
||
get_settings.cache_clear()
|
||
try:
|
||
yield
|
||
finally:
|
||
get_settings.cache_clear()
|
||
|
||
|
||
@pytest.fixture
|
||
def _all_models_downloaded(monkeypatch: pytest.MonkeyPatch) -> None:
|
||
"""Считать все модели всех уровней скачанными (изолирует тест от реального диска)."""
|
||
monkeypatch.setattr(ai_levels, "_model_downloaded", lambda _path: True)
|
||
|
||
|
||
@pytest.fixture
|
||
def _no_models_downloaded(monkeypatch: pytest.MonkeyPatch) -> None:
|
||
"""Считать, что ни одна модель не скачана."""
|
||
monkeypatch.setattr(ai_levels, "_model_downloaded", lambda _path: False)
|
||
|
||
|
||
def test_settings_treats_empty_hw_env_strings_as_none(monkeypatch: pytest.MonkeyPatch) -> None:
|
||
"""`docker-compose`/`env_file` подставляют `KEY=` из `.env` как пустую строку, не как
|
||
отсутствие переменной: `install.sh` пишет `HW_VRAM_MB=` пустым на любой машине без
|
||
NVIDIA GPU (пресеты 1–4), `.env.example` — все четыре `HW_*` пустыми по умолчанию.
|
||
Без нормализации пустой строки в `None` `Settings()` падает `ValidationError` уже на
|
||
импорте (`main.py`, `workers/celery_app.py`) — регрессионный тест."""
|
||
monkeypatch.setenv("HW_CPUS", "")
|
||
monkeypatch.setenv("HW_RAM_MB", "")
|
||
monkeypatch.setenv("HW_GPU_NAME", "")
|
||
monkeypatch.setenv("HW_VRAM_MB", "")
|
||
|
||
settings = Settings()
|
||
|
||
assert settings.hw_cpus is None
|
||
assert settings.hw_ram_mb is None
|
||
assert settings.hw_gpu_name is None
|
||
assert settings.hw_vram_mb is None
|
||
|
||
|
||
def test_detect_ai_levels_returns_exactly_three_levels() -> None:
|
||
statuses = detect_ai_levels(_cfg())
|
||
|
||
assert {status.level for status in statuses} == {"min", "medium", "max"}
|
||
|
||
|
||
def test_min_available_with_enough_ram_and_models(
|
||
monkeypatch: pytest.MonkeyPatch, _all_models_downloaded: None
|
||
) -> None:
|
||
monkeypatch.setenv("HW_RAM_MB", "16384")
|
||
get_settings.cache_clear()
|
||
|
||
statuses = {status.level: status for status in detect_ai_levels(_cfg())}
|
||
|
||
assert statuses["min"].available is True
|
||
assert statuses["min"].reason is None
|
||
|
||
|
||
def test_min_unavailable_when_model_not_downloaded(_no_models_downloaded: None) -> None:
|
||
statuses = {status.level: status for status in detect_ai_levels(_cfg())}
|
||
|
||
assert statuses["min"].available is False
|
||
reason = statuses["min"].reason
|
||
assert reason is not None
|
||
assert "не скачана" in reason
|
||
assert "пресетом 3" in reason
|
||
|
||
|
||
def test_medium_unavailable_with_insufficient_ram(
|
||
monkeypatch: pytest.MonkeyPatch, _all_models_downloaded: None
|
||
) -> None:
|
||
monkeypatch.setenv("HW_RAM_MB", "16384") # хватает на min (16 ГБ), мало для medium (32 ГБ)
|
||
get_settings.cache_clear()
|
||
|
||
statuses = {status.level: status for status in detect_ai_levels(_cfg())}
|
||
|
||
assert statuses["medium"].available is False
|
||
reason = statuses["medium"].reason
|
||
assert reason is not None
|
||
assert "недостаточно RAM: нужно 32 ГБ" in reason
|
||
|
||
|
||
def test_medium_available_with_enough_ram_and_models_without_gpu(
|
||
monkeypatch: pytest.MonkeyPatch, _all_models_downloaded: None
|
||
) -> None:
|
||
"""`medium` не требует GPU (ADR-004: GPU опционален) — доступен на чистом CPU-железе."""
|
||
monkeypatch.setenv("HW_RAM_MB", "32768")
|
||
get_settings.cache_clear()
|
||
|
||
statuses = {status.level: status for status in detect_ai_levels(_cfg())}
|
||
|
||
assert statuses["medium"].available is True
|
||
assert statuses["medium"].reason is None
|
||
|
||
|
||
def test_medium_unavailable_when_model_not_downloaded(
|
||
monkeypatch: pytest.MonkeyPatch, _no_models_downloaded: None
|
||
) -> None:
|
||
monkeypatch.setenv("HW_RAM_MB", "32768")
|
||
get_settings.cache_clear()
|
||
|
||
statuses = {status.level: status for status in detect_ai_levels(_cfg())}
|
||
|
||
assert statuses["medium"].available is False
|
||
reason = statuses["medium"].reason
|
||
assert reason is not None
|
||
assert "не скачана" in reason
|
||
assert "пресетом 4" in reason
|
||
|
||
|
||
def test_max_unavailable_without_gpu(
|
||
monkeypatch: pytest.MonkeyPatch, _all_models_downloaded: None
|
||
) -> None:
|
||
monkeypatch.setenv("HW_RAM_MB", "65536")
|
||
get_settings.cache_clear()
|
||
|
||
statuses = {status.level: status for status in detect_ai_levels(_cfg())}
|
||
|
||
assert statuses["max"].available is False
|
||
reason = statuses["max"].reason
|
||
assert reason is not None
|
||
assert "требуется GPU NVIDIA ≥16 ГБ VRAM, не обнаружен" in reason
|
||
|
||
|
||
def test_max_unavailable_with_insufficient_vram(
|
||
monkeypatch: pytest.MonkeyPatch, _all_models_downloaded: None
|
||
) -> None:
|
||
monkeypatch.setenv("HW_RAM_MB", "65536")
|
||
monkeypatch.setenv("HW_GPU_NAME", "NVIDIA RTX 3060")
|
||
monkeypatch.setenv("HW_VRAM_MB", "12288")
|
||
get_settings.cache_clear()
|
||
|
||
statuses = {status.level: status for status in detect_ai_levels(_cfg())}
|
||
|
||
assert statuses["max"].available is False
|
||
reason = statuses["max"].reason
|
||
assert reason is not None
|
||
assert "обнаружено только 12 ГБ" in reason
|
||
|
||
|
||
def test_max_available_with_gpu_ram_and_models(
|
||
monkeypatch: pytest.MonkeyPatch, _all_models_downloaded: None
|
||
) -> None:
|
||
monkeypatch.setenv("HW_RAM_MB", "65536")
|
||
monkeypatch.setenv("HW_GPU_NAME", "NVIDIA RTX 4090")
|
||
monkeypatch.setenv("HW_VRAM_MB", "24576")
|
||
get_settings.cache_clear()
|
||
|
||
statuses = {status.level: status for status in detect_ai_levels(_cfg())}
|
||
|
||
assert statuses["max"].available is True
|
||
assert statuses["max"].reason is None
|