fix(tests): изолировать тесты от instance_settings в общей dev-БД
Тесты читали ту же instance_settings, что и dev-стенд: выключение chat/hand_queue в админке роняло пачку тестов, не связанных с самим переключением (наступила сессия 0.0.28). Фикстура clean_instance_settings (явная, не autouse — DELETE в savepoint держит блокировку строки до конца внешней транзакции теста, автовключение на тестах с отдельными подключениями к БД дало саморазблокировку) удаляет управляемые ключи перед тестом, чтение конфигурации падает на дефолты pydantic-моделей. Подключена в test_chat_ws.py и test_hand_queue_ws.py. MANAGED_KEYS в services/instance_settings.py — единый список управляемых ключей вместо локальной копии в тестовом файле.
This commit is contained in:
@@ -21,10 +21,11 @@ from sqlalchemy.dialects.postgresql import insert as pg_insert
|
||||
from sqlalchemy.ext.asyncio import AsyncConnection, AsyncSession
|
||||
from starlette.types import Message, Scope
|
||||
|
||||
from core.db import engine, get_session
|
||||
from core.db import engine, get_metrics_session, get_session
|
||||
from core.redis import redis_client
|
||||
from main import create_app
|
||||
from models.instance_setting import InstanceSetting
|
||||
from services.instance_settings import MANAGED_KEYS
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(autouse=True)
|
||||
@@ -88,6 +89,37 @@ async def _load_committed_instance_settings() -> dict[str, Any]:
|
||||
return {key: value for key, value in result.all()}
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def clean_instance_settings(db_session: AsyncSession) -> AsyncGenerator[None, None]:
|
||||
"""Изолировать тест от текущего состояния управляемых ключей `instance_settings` в
|
||||
общей dev-БД (тоггл чата, поднятия руки и т.п. — живые настройки разработчика, а не
|
||||
тестовые данные).
|
||||
|
||||
Удаляет строки `MANAGED_KEYS` внутри savepoint-транзакции теста (`db_session`) —
|
||||
последующее чтение конфигурации (`InstanceSettingsService.get`/`load_effective_config`)
|
||||
падает на дефолты pydantic-моделей (например, `ChatConfig.enabled == True`), одинаковые
|
||||
независимо от того, что реально сохранено в dev-БД в момент прогона. Savepoint
|
||||
откатывается в `db_connection` по завершении теста — восстанавливать исходные строки
|
||||
вручную не нужно, в отличие от `_preserve_instance_settings` (та фикстура страхует от
|
||||
записи МИМО savepoint, эта — от чтения ИЗ него состояния, унаследованного от dev-БД).
|
||||
|
||||
НЕ autouse и намеренно: `DELETE` внутри savepoint держит Postgres-блокировку на
|
||||
строке до конца ВНЕШНЕЙ транзакции теста (`db_connection`, откатывается только в
|
||||
teardown) — savepoint её не освобождает раньше срока. Тесты, которые параллельно
|
||||
внутри СЕБЯ же пишут в те же ключи через ОТДЕЛЬНОЕ реальное подключение
|
||||
(`async_session_maker`/`engine.connect()` — см. `test_transcription_disabled_setting_
|
||||
stops_run_pipeline`, `clean_bootstrap_managed_keys`), заблокировались бы сами на себе,
|
||||
если бы эта фикстура применялась к ним автоматически (наступили при первой попытке
|
||||
сделать её autouse — само-дедлок, тест висел до ручного убийства процесса). Поэтому
|
||||
запрашивать явно, только в тестах, где именно ОНА обеспечивает изоляцию (WS чата/
|
||||
очереди рук и т.п.), а не там, где тест сам управляет состоянием через реальные
|
||||
коммиты.
|
||||
"""
|
||||
await db_session.execute(delete(InstanceSetting).where(InstanceSetting.key.in_(MANAGED_KEYS)))
|
||||
await db_session.commit()
|
||||
yield
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def db_connection() -> AsyncGenerator[AsyncConnection, None]:
|
||||
async with engine.connect() as connection:
|
||||
|
||||
@@ -7,13 +7,14 @@ websocket-тестов поверх нашей savepoint-сессии БД (см
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from collections.abc import Callable
|
||||
from collections.abc import AsyncGenerator, Callable
|
||||
from datetime import UTC, datetime
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
import jwt
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
from pydantic import ValidationError
|
||||
from sqlalchemy import select, text
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
@@ -37,6 +38,16 @@ from tests.conftest import ASGIWebSocketSession
|
||||
WSFactory = Callable[[str], ASGIWebSocketSession]
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(autouse=True)
|
||||
async def _isolated_instance_settings(clean_instance_settings: None) -> AsyncGenerator[None, None]:
|
||||
"""Autouse только в этом модуле — изолирует тесты от состояния `chat`/`hand_queue`
|
||||
(и остальных управляемых ключей) в общей dev-БД (см. `tests.conftest.clean_instance_settings`,
|
||||
почему не сделана глобально autouse). Безопасно именно здесь: ни один тест файла не
|
||||
открывает отдельного подключения к `instance_settings` — только `db_session`/`client`.
|
||||
"""
|
||||
yield
|
||||
|
||||
|
||||
# --- Хелперы ---------------------------------------------------------------
|
||||
|
||||
|
||||
|
||||
@@ -5,10 +5,11 @@
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from collections.abc import Callable
|
||||
from collections.abc import AsyncGenerator, Callable
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
import pytest_asyncio
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from core.security import create_access_token, hash_password
|
||||
@@ -23,6 +24,13 @@ from tests.conftest import ASGIWebSocketSession
|
||||
WSFactory = Callable[[str], ASGIWebSocketSession]
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(autouse=True)
|
||||
async def _isolated_instance_settings(clean_instance_settings: None) -> AsyncGenerator[None, None]:
|
||||
"""Autouse только в этом модуле — см. `tests.test_chat_ws._isolated_instance_settings`
|
||||
и `tests.conftest.clean_instance_settings` (почему не глобальный autouse)."""
|
||||
yield
|
||||
|
||||
|
||||
# --- Хелперы (см. tests/test_chat_ws.py) ------------------------------------
|
||||
|
||||
|
||||
|
||||
@@ -12,15 +12,12 @@
|
||||
видеть реально закоммиченную строку (см. docstring `test_pipeline.py`),
|
||||
поэтому там запись делается через отдельное подключение к `core.db.engine`.
|
||||
|
||||
Тесты, которым нужен «чистый стол» по управляемым ключам `instance_settings`
|
||||
(бутстрап дефолтов, дефолтные значения после патча), используют фикстуру
|
||||
`clean_instance_settings` — она сохраняет текущие строки этих ключей в
|
||||
рамках транзакции теста и восстанавливает их после (не `TRUNCATE`): в общей
|
||||
dev-БД эти строки могут быть легитимными данными разработчика, тест не
|
||||
должен от них зависеть, но и не должен их безвозвратно стирать. Тест с
|
||||
`run_pipeline_async` не может использовать эту фикстуру (пишет через
|
||||
отдельное подключение) — там то же сохранение/восстановление сделано вручную
|
||||
через реальный коннекшн.
|
||||
«Чистый стол» по управляемым ключам `instance_settings` обеспечивает общая (НЕ
|
||||
autouse — см. её докстринг про само-дедлок с тестами на реальных подключениях)
|
||||
фикстура `tests.conftest.clean_instance_settings` — тесты, которым нужен чистый
|
||||
стол, запрашивают её явно параметром. Тест с `run_pipeline_async` пишет через
|
||||
отдельное подключение (мимо savepoint) и её не запрашивает — там сохранение/
|
||||
восстановление сделано вручную через реальный коннекшн, см. его докстринг.
|
||||
"""
|
||||
|
||||
import uuid
|
||||
@@ -61,24 +58,6 @@ from workers.tasks.pipeline import run_pipeline_async
|
||||
PLUGINS_YAML = "../config/plugins.yaml"
|
||||
NOW = datetime.now(UTC)
|
||||
|
||||
# Все ключи, которыми управляет `InstanceSettingsService` (см. `_KEY_*` там же).
|
||||
_MANAGED_KEYS = (
|
||||
"transcriber",
|
||||
"summarizer",
|
||||
"chat",
|
||||
"hand_queue",
|
||||
"ai_level",
|
||||
"summary_recipients",
|
||||
"display_timezone",
|
||||
"registration_team_choice",
|
||||
"registration_email_domain",
|
||||
"contact_email",
|
||||
"media_limits",
|
||||
"consent_policy",
|
||||
"device_check",
|
||||
"virtual_background",
|
||||
)
|
||||
|
||||
|
||||
class _FakeTask:
|
||||
"""Минимальная заглушка bound-задачи Celery (см. `test_pipeline.py`)."""
|
||||
@@ -87,32 +66,6 @@ class _FakeTask:
|
||||
self.retry = MagicMock()
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def clean_instance_settings(db_session: AsyncSession) -> AsyncGenerator[None, None]:
|
||||
"""Изолировать тест от уже существующих строк управляемых ключей `instance_settings`.
|
||||
|
||||
Сохраняет текущие значения (если есть) в рамках `db_session` (savepoint,
|
||||
никогда не коммитится в реальную БД — см. `conftest.py`), удаляет их,
|
||||
отдаёт управление тесту, затем восстанавливает исходные значения —
|
||||
точечно, только эти ключи, не `TRUNCATE`.
|
||||
"""
|
||||
result = await db_session.execute(
|
||||
select(InstanceSetting).where(InstanceSetting.key.in_(_MANAGED_KEYS))
|
||||
)
|
||||
saved: dict[str, Any] = {row.key: row.value for row in result.scalars().all()}
|
||||
await db_session.execute(delete(InstanceSetting).where(InstanceSetting.key.in_(_MANAGED_KEYS)))
|
||||
await db_session.commit()
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
await db_session.execute(
|
||||
delete(InstanceSetting).where(InstanceSetting.key.in_(_MANAGED_KEYS))
|
||||
)
|
||||
for key, value in saved.items():
|
||||
db_session.add(InstanceSetting(key=key, value=value))
|
||||
await db_session.commit()
|
||||
|
||||
|
||||
async def test_ensure_bootstrapped_imports_yaml_defaults(
|
||||
db_session: AsyncSession, clean_instance_settings: None
|
||||
) -> None:
|
||||
|
||||
Reference in New Issue
Block a user