119 lines
5.1 KiB
Python
119 lines
5.1 KiB
Python
"""Общая настройка тестов.
|
||
|
||
Голосовой контур выключен: он грузит модели около десяти секунд, а проверяется
|
||
отдельно в `test_voice_pipeline.py`. Вход включён как на стенде, но тесты
|
||
логинятся через `dev-token` — так же, как `make lesson` (tasks/lct-23-auth-roles.md).
|
||
"""
|
||
|
||
import copy
|
||
import os
|
||
import socket
|
||
from pathlib import Path
|
||
from urllib.parse import urlparse
|
||
|
||
os.environ.setdefault("DEV_AUTH_BYPASS", "true")
|
||
# Тесты ждут локальную модель по умолчанию. Личный backend/.env с облачным
|
||
# провайдером иначе валит проверки офлайн-гарда; переменные окружения
|
||
# перекрывают .env, а живые проверки test_llm.py выставляют свои поверх.
|
||
# Рекомендации Vikhr выключены: на WSL2 подключение к закрытому loopback-порту
|
||
# висит до таймаута, и финиш занятия ждал бы 6 с. Тесты рекомендаций
|
||
# подменяют coach сами.
|
||
os.environ.update({
|
||
"LLM_PROVIDER": "local",
|
||
"LLM_BASE_URL": "http://127.0.0.1:18080/v1",
|
||
"LLM_API_KEY": "",
|
||
"LLM_MODEL_CALLER": "Qwen3-1.7B",
|
||
"LLM_MODEL_JUDGE": "",
|
||
"ASSESSMENT_FEEDBACK_ENABLED": "false",
|
||
})
|
||
|
||
import pytest # noqa: E402
|
||
|
||
from app.config import get_settings # noqa: E402
|
||
|
||
|
||
@pytest.fixture
|
||
def postgres_access():
|
||
"""Skip DB integration cases when the configured PostgreSQL is unreachable.
|
||
|
||
`/api/health` is a liveness endpoint and deliberately does not probe the
|
||
database. Use a short TCP check so sandbox/network-denied runs are reported
|
||
as unverified integration tests instead of misleading application failures.
|
||
"""
|
||
url = urlparse(get_settings().database_url)
|
||
if url.scheme not in {"postgres", "postgresql", "postgresql+asyncpg"}:
|
||
pytest.skip("PostgreSQL integration test requires a PostgreSQL DATABASE_URL")
|
||
try:
|
||
with socket.create_connection((url.hostname or "localhost", url.port or 5432), timeout=2):
|
||
pass
|
||
except OSError as exc:
|
||
pytest.skip(f"PostgreSQL unavailable ({exc})")
|
||
|
||
|
||
@pytest.fixture(scope="module")
|
||
def template_caller():
|
||
"""Звонящий без модели: сквозные тесты занятия ждут заготовки и офлайн-
|
||
ответы. С моделью по умолчанию (loopback Qwen) фабрика выбрала бы
|
||
LlmCaller и ждала неподнятый сервер."""
|
||
from app.dialog import factory
|
||
|
||
settings = get_settings().model_copy(update={"llm_provider": "disabled"})
|
||
with pytest.MonkeyPatch.context() as patch:
|
||
patch.setattr(factory, "get_settings", lambda: settings)
|
||
yield
|
||
|
||
|
||
@pytest.fixture(autouse=True, scope="session")
|
||
def _dev_auth():
|
||
"""Флаг обхода читается один раз при создании настроек."""
|
||
get_settings.cache_clear()
|
||
settings = get_settings()
|
||
assert settings.dev_auth_bypass, "тесты должны ходить через dev-token"
|
||
yield
|
||
|
||
|
||
@pytest.fixture(autouse=True, scope="session")
|
||
def _parsed_yaml_once():
|
||
"""Каждый `TestClient` на старте перечитывает всю библиотеку сценариев:
|
||
~200 YAML — это 0.2 с на тест и минута на прогон. Разбор кешируется на
|
||
сессию; проверка схемы и сборка эталона идут по-прежнему на каждый старт.
|
||
Копия — чтобы правка словаря в одном тесте не протекла в другой.
|
||
"""
|
||
from app.scenarios import loader
|
||
|
||
original = loader._read_yaml
|
||
cache: dict[tuple[Path, int], dict] = {}
|
||
|
||
def cached(path: Path) -> dict:
|
||
key = (path.resolve(), path.stat().st_mtime_ns)
|
||
if key not in cache:
|
||
cache[key] = original(path)
|
||
return copy.deepcopy(cache[key])
|
||
|
||
loader._read_yaml = cached
|
||
yield
|
||
loader._read_yaml = original
|
||
|
||
|
||
@pytest.fixture(autouse=True)
|
||
def _fresh_engine():
|
||
"""Движок базы — на тест, а не на процесс.
|
||
|
||
Пул соединений привязан к событийному циклу, в котором создан, а каждый
|
||
`TestClient` поднимает свой. Общий движок даёт «Future attached to
|
||
a different loop» на втором обращении к базе — тест падает не там, где
|
||
ошибка, и только в компании других тестов.
|
||
"""
|
||
from app.db import base
|
||
|
||
base.reset()
|
||
yield
|
||
base.reset()
|
||
|
||
|
||
@pytest.fixture
|
||
def as_instructor(client):
|
||
"""Клиент, вошедший преподавателем. Cookie живёт в самом TestClient."""
|
||
response = client.post("/api/auth/dev-token")
|
||
assert response.status_code == 200, response.text
|
||
return client
|