Первоначальная версия VidConf
This commit is contained in:
652
backend/tests/test_instance_settings.py
Normal file
652
backend/tests/test_instance_settings.py
Normal file
@@ -0,0 +1,652 @@
|
||||
"""Тесты хранилища настроек инстанса (`services.instance_settings`).
|
||||
|
||||
Бутстрап из yaml однократен (повторный
|
||||
вызов не перетирает правки администратора); fallback воркера на yaml при
|
||||
пустой таблице; `transcription_enabled=false` в БД → следующий `run_pipeline`
|
||||
останавливается после чтения конфигурации без ошибок; частичный PUT.
|
||||
|
||||
Тесты сервисного уровня (`db_session`, savepoint-транзакция — не видна другим
|
||||
подключениям, никогда не коммитится в реальную БД, см. `conftest.py`)
|
||||
используются везде, кроме интеграционного теста с `run_pipeline_async` — та
|
||||
задача открывает собственное подключение (`workers.db.open_session`) и должна
|
||||
видеть реально закоммиченную строку (см. docstring `test_pipeline.py`),
|
||||
поэтому там запись делается через отдельное подключение к `core.db.engine`.
|
||||
|
||||
Тесты, которым нужен «чистый стол» по управляемым ключам `instance_settings`
|
||||
(бутстрап дефолтов, дефолтные значения после патча), используют фикстуру
|
||||
`clean_instance_settings` — она сохраняет текущие строки этих ключей в
|
||||
рамках транзакции теста и восстанавливает их после (не `TRUNCATE`): в общей
|
||||
dev-БД эти строки могут быть легитимными данными разработчика, тест не
|
||||
должен от них зависеть, но и не должен их безвозвратно стирать. Тест с
|
||||
`run_pipeline_async` не может использовать эту фикстуру (пишет через
|
||||
отдельное подключение) — там то же сохранение/восстановление сделано вручную
|
||||
через реальный коннекшн.
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from collections.abc import AsyncGenerator
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
from sqlalchemy import delete, select, text
|
||||
from sqlalchemy.dialects.postgresql import insert as pg_insert
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from core.config import get_settings
|
||||
from core.db import async_session_maker, engine
|
||||
from core.plugins.config import InstanceConfig, load_plugins_config
|
||||
from models.instance_setting import InstanceSetting
|
||||
from scripts import apply_preset_settings as apply_preset_settings_module
|
||||
from services.ai_levels import detect_ai_levels
|
||||
from services.ai_tiers import TIERS
|
||||
from services.conference_ids import generate_number, generate_slug
|
||||
from services.instance_settings import (
|
||||
BOOTSTRAP_MANAGED_KEYS,
|
||||
BootstrapOverrides,
|
||||
InstanceSettingsService,
|
||||
InvalidAiLevelError,
|
||||
InvalidEmailDomainError,
|
||||
InvalidTimezoneError,
|
||||
SettingsUpdateIn,
|
||||
load_effective_config,
|
||||
)
|
||||
from workers.tasks import pipeline as pipeline_module
|
||||
from workers.tasks.pipeline import run_pipeline_async
|
||||
|
||||
PLUGINS_YAML = "../config/plugins.yaml"
|
||||
NOW = datetime.now(UTC)
|
||||
|
||||
# Все ключи, которыми управляет `InstanceSettingsService` (см. `_KEY_*` там же).
|
||||
_MANAGED_KEYS = (
|
||||
"transcriber",
|
||||
"summarizer",
|
||||
"chat",
|
||||
"ai_level",
|
||||
"summary_recipients",
|
||||
"display_timezone",
|
||||
"registration_team_choice",
|
||||
"registration_email_domain",
|
||||
)
|
||||
|
||||
|
||||
class _FakeTask:
|
||||
"""Минимальная заглушка bound-задачи Celery (см. `test_pipeline.py`)."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
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:
|
||||
service = InstanceSettingsService(db_session)
|
||||
|
||||
await service.ensure_bootstrapped(PLUGINS_YAML)
|
||||
|
||||
rows = (await db_session.execute(select(InstanceSetting))).scalars().all()
|
||||
keys = {row.key for row in rows}
|
||||
assert keys == {
|
||||
"transcriber",
|
||||
"summarizer",
|
||||
"chat",
|
||||
"ai_level",
|
||||
"summary_recipients",
|
||||
"display_timezone",
|
||||
"registration_team_choice",
|
||||
"registration_email_domain",
|
||||
}
|
||||
cfg = await service.get()
|
||||
assert cfg.transcriber.provider == "faster_whisper_cpu"
|
||||
assert cfg.ai_level == "min"
|
||||
assert cfg.summary_recipients == "all"
|
||||
assert cfg.display_timezone == "Europe/Moscow"
|
||||
assert cfg.registration_team_choice is False
|
||||
assert cfg.registration_email_domain_enabled is False
|
||||
assert cfg.registration_email_domain is None
|
||||
|
||||
|
||||
async def test_ensure_bootstrapped_is_idempotent_and_keeps_admin_edits(
|
||||
db_session: AsyncSession, clean_instance_settings: None
|
||||
) -> None:
|
||||
service = InstanceSettingsService(db_session)
|
||||
await service.ensure_bootstrapped(PLUGINS_YAML)
|
||||
|
||||
await service.update(SettingsUpdateIn(display_timezone="Asia/Yekaterinburg"))
|
||||
|
||||
# Повторный бутстрап (например, рестарт backend) не должен перетереть
|
||||
# уже сделанную администратором правку — `ON CONFLICT DO NOTHING`.
|
||||
await service.ensure_bootstrapped(PLUGINS_YAML)
|
||||
|
||||
cfg = await service.get()
|
||||
assert cfg.display_timezone == "Asia/Yekaterinburg"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("preset", "chat_enabled", "ai_enabled", "ai_level"),
|
||||
[
|
||||
(1, False, False, "min"),
|
||||
(2, True, False, "min"),
|
||||
(3, True, True, "min"),
|
||||
(4, True, True, "medium"),
|
||||
(5, True, True, "max"),
|
||||
],
|
||||
)
|
||||
async def test_ensure_bootstrapped_applies_installer_preset_matrix(
|
||||
db_session: AsyncSession,
|
||||
clean_instance_settings: None,
|
||||
preset: int,
|
||||
chat_enabled: bool,
|
||||
ai_enabled: bool,
|
||||
ai_level: str,
|
||||
) -> None:
|
||||
"""Матрица «пресет → настройки»:
|
||||
чистая БД бутстрапится настройками пресета, а не всегда-включёнными
|
||||
дефолтами `plugins.yaml`."""
|
||||
service = InstanceSettingsService(db_session)
|
||||
overrides = BootstrapOverrides(
|
||||
chat_enabled=chat_enabled, ai_enabled=ai_enabled, ai_level=ai_level
|
||||
)
|
||||
|
||||
await service.ensure_bootstrapped(PLUGINS_YAML, overrides)
|
||||
|
||||
cfg = await service.get()
|
||||
assert cfg.chat.enabled is chat_enabled, f"пресет {preset}: chat.enabled"
|
||||
assert cfg.transcriber.enabled is ai_enabled, f"пресет {preset}: transcriber.enabled"
|
||||
assert cfg.summarizer.enabled is ai_enabled, f"пресет {preset}: summarizer.enabled"
|
||||
assert cfg.ai_level == ai_level, f"пресет {preset}: ai_level"
|
||||
|
||||
|
||||
async def test_ensure_bootstrapped_overrides_do_not_touch_existing_rows(
|
||||
db_session: AsyncSession, clean_instance_settings: None
|
||||
) -> None:
|
||||
"""Повторный запуск инсталлятора (другой пресет) на живой инсталляции не
|
||||
перетирает уже существующие строки — только чистая БД получает overrides
|
||||
(`ON CONFLICT DO NOTHING` сохраняется, см. `ensure_bootstrapped`)."""
|
||||
service = InstanceSettingsService(db_session)
|
||||
await service.ensure_bootstrapped(PLUGINS_YAML) # дефолты plugins.yaml — чат включён
|
||||
await service.update(SettingsUpdateIn(chat_enabled=False)) # ручная правка администратора
|
||||
|
||||
preset_1_overrides = BootstrapOverrides(chat_enabled=True, ai_enabled=False, ai_level="min")
|
||||
await service.ensure_bootstrapped(PLUGINS_YAML, preset_1_overrides)
|
||||
|
||||
cfg = await service.get()
|
||||
assert cfg.chat.enabled is False
|
||||
|
||||
|
||||
async def test_load_effective_config_falls_back_to_yaml_when_table_empty(
|
||||
db_session: AsyncSession, clean_instance_settings: None
|
||||
) -> None:
|
||||
"""Таблица `instance_settings` пуста (воркер стартовал раньше бутстрапа backend)."""
|
||||
cfg = await load_effective_config(db_session)
|
||||
|
||||
assert isinstance(cfg, InstanceConfig)
|
||||
assert cfg.transcriber.provider == "faster_whisper_cpu"
|
||||
assert cfg.ai_level == "min"
|
||||
|
||||
|
||||
async def test_load_effective_config_applies_tier_override_for_medium_and_max(
|
||||
db_session: AsyncSession, clean_instance_settings: None
|
||||
) -> None:
|
||||
"""При `ai_level` `medium`/`max` `load_effective_config` подставляет провайдера/модель
|
||||
из `TIERS[level]` (ADR-004), а не дефолты `plugins.yaml`.
|
||||
|
||||
Запись `ai_level` — напрямую в `db_session` (не через `service.update`,
|
||||
которая отклонила бы недоступный на этой машине уровень) — здесь
|
||||
проверяется чтение/подстановка, а не бизнес-правило доступности
|
||||
(см. `test_ai_levels.py`, `test_update_rejects_unavailable_ai_level`)."""
|
||||
service = InstanceSettingsService(db_session)
|
||||
await service.ensure_bootstrapped(PLUGINS_YAML)
|
||||
stmt = (
|
||||
pg_insert(InstanceSetting)
|
||||
.values(key="ai_level", value={"level": "medium"})
|
||||
.on_conflict_do_update(index_elements=["key"], set_={"value": {"level": "medium"}})
|
||||
)
|
||||
await db_session.execute(stmt)
|
||||
await db_session.commit()
|
||||
|
||||
cfg = await load_effective_config(db_session)
|
||||
|
||||
assert cfg.transcriber.provider == TIERS["medium"].transcriber.provider
|
||||
assert cfg.transcriber.model == TIERS["medium"].transcriber.model
|
||||
assert cfg.summarizer.provider == "qwen_local"
|
||||
assert cfg.summarizer.model == TIERS["medium"].summarizer.model
|
||||
assert (
|
||||
cfg.summarizer.options["max_tokens_reduce"]
|
||||
== TIERS["medium"].summarizer.options["max_tokens_reduce"]
|
||||
)
|
||||
|
||||
|
||||
async def test_load_effective_config_min_level_keeps_yaml_defaults(
|
||||
db_session: AsyncSession, clean_instance_settings: None
|
||||
) -> None:
|
||||
"""`ai_level=min` (дефолт бутстрапа) не подменяется `TIERS` — используется
|
||||
конфигурация из `plugins.yaml`/правок администратора как раньше."""
|
||||
service = InstanceSettingsService(db_session)
|
||||
await service.ensure_bootstrapped(PLUGINS_YAML)
|
||||
|
||||
cfg = await load_effective_config(db_session)
|
||||
|
||||
assert cfg.transcriber.provider == "faster_whisper_cpu"
|
||||
assert cfg.summarizer.provider == "null" # дефолт plugins.yaml вне подмены
|
||||
|
||||
|
||||
async def test_load_effective_config_tier_override_preserves_enabled_flag(
|
||||
db_session: AsyncSession, clean_instance_settings: None
|
||||
) -> None:
|
||||
"""Подмена `TIERS` не включает повторно транскрибацию/суммаризацию, отключённую
|
||||
администратором (`transcription_enabled=false`) — флаг `enabled` сохраняется."""
|
||||
service = InstanceSettingsService(db_session)
|
||||
await service.ensure_bootstrapped(PLUGINS_YAML)
|
||||
await service.update(SettingsUpdateIn(transcription_enabled=False))
|
||||
stmt = (
|
||||
pg_insert(InstanceSetting)
|
||||
.values(key="ai_level", value={"level": "max"})
|
||||
.on_conflict_do_update(index_elements=["key"], set_={"value": {"level": "max"}})
|
||||
)
|
||||
await db_session.execute(stmt)
|
||||
await db_session.commit()
|
||||
|
||||
cfg = await load_effective_config(db_session)
|
||||
|
||||
assert cfg.transcriber.provider == TIERS["max"].transcriber.provider
|
||||
assert cfg.transcriber.enabled is False
|
||||
assert cfg.summarizer.enabled is False
|
||||
|
||||
|
||||
async def test_partial_update_changes_only_requested_fields(
|
||||
db_session: AsyncSession, clean_instance_settings: None
|
||||
) -> None:
|
||||
service = InstanceSettingsService(db_session)
|
||||
await service.ensure_bootstrapped(PLUGINS_YAML)
|
||||
|
||||
cfg = await service.update(SettingsUpdateIn(chat_enabled=False))
|
||||
|
||||
assert cfg.chat.enabled is False
|
||||
# Остальные поля не тронуты дефолтами бутстрапа.
|
||||
assert cfg.transcriber.enabled is True
|
||||
assert cfg.summarizer.enabled is True
|
||||
assert cfg.ai_level == "min"
|
||||
assert cfg.summary_recipients == "all"
|
||||
assert cfg.display_timezone == "Europe/Moscow"
|
||||
|
||||
# Изменение сохранилось в БД (не только в возвращённом объекте).
|
||||
reloaded = await service.get()
|
||||
assert reloaded.chat.enabled is False
|
||||
|
||||
|
||||
async def test_registration_team_choice_toggle(
|
||||
db_session: AsyncSession, clean_instance_settings: None
|
||||
) -> None:
|
||||
"""`registration_team_choice` — дефолт `False`, `PUT`-патч переключает и сохраняется."""
|
||||
service = InstanceSettingsService(db_session)
|
||||
await service.ensure_bootstrapped(PLUGINS_YAML)
|
||||
assert (await service.get()).registration_team_choice is False
|
||||
|
||||
cfg = await service.update(SettingsUpdateIn(registration_team_choice=True))
|
||||
assert cfg.registration_team_choice is True
|
||||
|
||||
reloaded = await service.get()
|
||||
assert reloaded.registration_team_choice is True
|
||||
|
||||
|
||||
async def test_registration_email_domain_enable_without_domain_rejected(
|
||||
db_session: AsyncSession, clean_instance_settings: None
|
||||
) -> None:
|
||||
"""Включение верификации без домена (ни в патче, ни ранее сохранённого) → 400."""
|
||||
service = InstanceSettingsService(db_session)
|
||||
await service.ensure_bootstrapped(PLUGINS_YAML)
|
||||
|
||||
with pytest.raises(InvalidEmailDomainError):
|
||||
await service.update(SettingsUpdateIn(registration_email_domain_enabled=True))
|
||||
|
||||
cfg = await service.get()
|
||||
assert cfg.registration_email_domain_enabled is False
|
||||
assert cfg.registration_email_domain is None
|
||||
|
||||
|
||||
async def test_registration_email_domain_rejects_invalid_pattern(
|
||||
db_session: AsyncSession, clean_instance_settings: None
|
||||
) -> None:
|
||||
service = InstanceSettingsService(db_session)
|
||||
await service.ensure_bootstrapped(PLUGINS_YAML)
|
||||
|
||||
with pytest.raises(InvalidEmailDomainError):
|
||||
await service.update(SettingsUpdateIn(registration_email_domain="not a domain"))
|
||||
|
||||
cfg = await service.get()
|
||||
assert cfg.registration_email_domain is None
|
||||
|
||||
|
||||
async def test_registration_email_domain_normalizes_input(
|
||||
db_session: AsyncSession, clean_instance_settings: None
|
||||
) -> None:
|
||||
"""`@Corp.RU ` нормализуется в `corp.ru` (strip, убрать «@», lower)."""
|
||||
service = InstanceSettingsService(db_session)
|
||||
await service.ensure_bootstrapped(PLUGINS_YAML)
|
||||
|
||||
cfg = await service.update(
|
||||
SettingsUpdateIn(
|
||||
registration_email_domain_enabled=True, registration_email_domain="@Corp.RU "
|
||||
)
|
||||
)
|
||||
|
||||
assert cfg.registration_email_domain_enabled is True
|
||||
assert cfg.registration_email_domain == "corp.ru"
|
||||
|
||||
reloaded = await service.get()
|
||||
assert reloaded.registration_email_domain == "corp.ru"
|
||||
|
||||
|
||||
async def test_registration_email_domain_can_be_disabled_keeping_stored_domain(
|
||||
db_session: AsyncSession, clean_instance_settings: None
|
||||
) -> None:
|
||||
"""Выключение верификации без передачи домена не требует домена и не роняет валидацию."""
|
||||
service = InstanceSettingsService(db_session)
|
||||
await service.ensure_bootstrapped(PLUGINS_YAML)
|
||||
await service.update(
|
||||
SettingsUpdateIn(
|
||||
registration_email_domain_enabled=True, registration_email_domain="acme.io"
|
||||
)
|
||||
)
|
||||
|
||||
cfg = await service.update(SettingsUpdateIn(registration_email_domain_enabled=False))
|
||||
|
||||
assert cfg.registration_email_domain_enabled is False
|
||||
assert cfg.registration_email_domain == "acme.io"
|
||||
|
||||
|
||||
async def test_transcription_enabled_flag_toggles_both_transcriber_and_summarizer(
|
||||
db_session: AsyncSession, clean_instance_settings: None
|
||||
) -> None:
|
||||
service = InstanceSettingsService(db_session)
|
||||
await service.ensure_bootstrapped(PLUGINS_YAML)
|
||||
|
||||
cfg = await service.update(SettingsUpdateIn(transcription_enabled=False))
|
||||
|
||||
assert cfg.transcriber.enabled is False
|
||||
assert cfg.summarizer.enabled is False
|
||||
|
||||
|
||||
async def test_update_rejects_unavailable_ai_level(
|
||||
db_session: AsyncSession, clean_instance_settings: None
|
||||
) -> None:
|
||||
service = InstanceSettingsService(db_session)
|
||||
await service.ensure_bootstrapped(PLUGINS_YAML)
|
||||
assert not detect_ai_levels(await service.get())[1].available # "medium" недоступен
|
||||
|
||||
with pytest.raises(InvalidAiLevelError):
|
||||
await service.update(SettingsUpdateIn(ai_level="medium"))
|
||||
|
||||
# Настройка не изменилась.
|
||||
cfg = await service.get()
|
||||
assert cfg.ai_level == "min"
|
||||
|
||||
|
||||
async def test_update_rejects_invalid_timezone(
|
||||
db_session: AsyncSession, clean_instance_settings: None
|
||||
) -> None:
|
||||
service = InstanceSettingsService(db_session)
|
||||
await service.ensure_bootstrapped(PLUGINS_YAML)
|
||||
|
||||
with pytest.raises(InvalidTimezoneError):
|
||||
await service.update(SettingsUpdateIn(display_timezone="Not/A_Timezone"))
|
||||
|
||||
cfg = await service.get()
|
||||
assert cfg.display_timezone == "Europe/Moscow"
|
||||
|
||||
|
||||
async def test_transcription_disabled_setting_stops_run_pipeline(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""`transcription_enabled=false`, закоммиченный в БД → `run_pipeline_async` без явного
|
||||
`plugins_config` читает эффективную конфигурацию сам и останавливается сразу,
|
||||
не трогая треки/статус, без исключений.
|
||||
|
||||
Строка `instance_settings.transcriber` пишется через отдельное реальное
|
||||
подключение (не через `db_session` — см. докстринг модуля), поэтому её
|
||||
исходное значение (если было — в общей dev-БД это может быть легитимными
|
||||
данными разработчика) сохраняется до записи и точечно восстанавливается
|
||||
в `finally`, а не удаляется безусловно.
|
||||
"""
|
||||
conference_id = uuid.uuid4()
|
||||
session_id = uuid.uuid4()
|
||||
number = generate_number()
|
||||
slug = generate_slug()
|
||||
t_start = NOW - timedelta(minutes=10)
|
||||
|
||||
async with engine.connect() as conn:
|
||||
await conn.execute(
|
||||
text(
|
||||
"INSERT INTO conferences (id, number, slug, title, status, is_pinned) "
|
||||
"VALUES (:id, :number, :slug, 'Effective config test', 'ended', false)"
|
||||
),
|
||||
{"id": conference_id, "number": number, "slug": slug},
|
||||
)
|
||||
await conn.execute(
|
||||
text(
|
||||
"INSERT INTO conference_sessions (id, conference_id, title, t_start, t_end) "
|
||||
"VALUES (:id, :conference_id, 'Effective config session', :t_start, :t_end)"
|
||||
),
|
||||
{
|
||||
"id": session_id,
|
||||
"conference_id": conference_id,
|
||||
"t_start": t_start,
|
||||
"t_end": NOW,
|
||||
},
|
||||
)
|
||||
await conn.commit()
|
||||
|
||||
disabled_transcriber = {
|
||||
"enabled": False,
|
||||
"provider": "null",
|
||||
"model": None,
|
||||
"language": "ru",
|
||||
"options": {},
|
||||
}
|
||||
async with async_session_maker() as setup_session:
|
||||
original_row = await setup_session.get(InstanceSetting, "transcriber")
|
||||
original_value: dict[str, Any] | None = (
|
||||
None if original_row is None else dict(original_row.value)
|
||||
)
|
||||
|
||||
stmt = (
|
||||
pg_insert(InstanceSetting)
|
||||
.values(key="transcriber", value=disabled_transcriber)
|
||||
.on_conflict_do_update(index_elements=["key"], set_={"value": disabled_transcriber})
|
||||
)
|
||||
await setup_session.execute(stmt)
|
||||
await setup_session.commit()
|
||||
|
||||
mock_factory = MagicMock()
|
||||
monkeypatch.setattr(pipeline_module, "create_transcriber", mock_factory)
|
||||
|
||||
try:
|
||||
await run_pipeline_async(_FakeTask(), session_id)
|
||||
|
||||
mock_factory.assert_not_called()
|
||||
async with engine.connect() as conn:
|
||||
result = await conn.execute(
|
||||
text("SELECT pipeline_status FROM conference_sessions WHERE id = :id"),
|
||||
{"id": session_id},
|
||||
)
|
||||
assert result.scalar_one() == "recording"
|
||||
finally:
|
||||
async with engine.connect() as conn:
|
||||
await conn.execute(
|
||||
text("DELETE FROM conference_sessions WHERE conference_id = :id"),
|
||||
{"id": conference_id},
|
||||
)
|
||||
await conn.execute(
|
||||
text("DELETE FROM conferences WHERE id = :id"), {"id": conference_id}
|
||||
)
|
||||
await conn.commit()
|
||||
|
||||
async with async_session_maker() as restore_session:
|
||||
if original_value is None:
|
||||
await restore_session.execute(
|
||||
delete(InstanceSetting).where(InstanceSetting.key == "transcriber")
|
||||
)
|
||||
else:
|
||||
restore_stmt = (
|
||||
pg_insert(InstanceSetting)
|
||||
.values(key="transcriber", value=original_value)
|
||||
.on_conflict_do_update(index_elements=["key"], set_={"value": original_value})
|
||||
)
|
||||
await restore_session.execute(restore_stmt)
|
||||
await restore_session.commit()
|
||||
|
||||
|
||||
# --- scripts.apply_preset_settings (принудительное обновление живой БД) -----------------
|
||||
#
|
||||
# `apply_preset_settings` открывает СОБСТВЕННУЮ сессию (`async_session_maker`, реальные
|
||||
# коммиты) — как и `run_pipeline_async` выше, не видит savepoint `db_session` теста и не
|
||||
# виден ей. Снимок 4 управляемых ключей снимается/восстанавливается отдельным реальным
|
||||
# подключением (тот же паттерн, что `test_transcription_disabled_setting_stops_run_pipeline`).
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def clean_bootstrap_managed_keys() -> AsyncGenerator[None, None]:
|
||||
"""Сохранить и точечно восстановить 4 ключа `BOOTSTRAP_MANAGED_KEYS` вокруг теста
|
||||
`scripts.apply_preset_settings` (пишет реальными коммитами мимо savepoint `db_session`)."""
|
||||
async with async_session_maker() as session:
|
||||
result = await session.execute(
|
||||
select(InstanceSetting).where(InstanceSetting.key.in_(BOOTSTRAP_MANAGED_KEYS))
|
||||
)
|
||||
saved: dict[str, Any] = {row.key: row.value for row in result.scalars().all()}
|
||||
await session.execute(
|
||||
delete(InstanceSetting).where(InstanceSetting.key.in_(BOOTSTRAP_MANAGED_KEYS))
|
||||
)
|
||||
await session.commit()
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
async with async_session_maker() as session:
|
||||
await session.execute(
|
||||
delete(InstanceSetting).where(InstanceSetting.key.in_(BOOTSTRAP_MANAGED_KEYS))
|
||||
)
|
||||
for key, value in saved.items():
|
||||
session.add(InstanceSetting(key=key, value=value))
|
||||
await session.commit()
|
||||
|
||||
|
||||
def _set_bootstrap_env(
|
||||
monkeypatch: pytest.MonkeyPatch, *, chat_enabled: bool, ai_enabled: bool, ai_level: str
|
||||
) -> None:
|
||||
monkeypatch.setenv("BOOTSTRAP_CHAT_ENABLED", str(chat_enabled).lower())
|
||||
monkeypatch.setenv("BOOTSTRAP_TRANSCRIPTION_ENABLED", str(ai_enabled).lower())
|
||||
monkeypatch.setenv("BOOTSTRAP_AI_LEVEL", ai_level)
|
||||
get_settings.cache_clear()
|
||||
|
||||
|
||||
async def _load_managed_rows() -> dict[str, Any]:
|
||||
async with async_session_maker() as session:
|
||||
result = await session.execute(
|
||||
select(InstanceSetting).where(InstanceSetting.key.in_(BOOTSTRAP_MANAGED_KEYS))
|
||||
)
|
||||
return {row.key: row.value for row in result.scalars().all()}
|
||||
|
||||
|
||||
async def test_apply_preset_settings_writes_missing_keys_without_force(
|
||||
monkeypatch: pytest.MonkeyPatch, clean_bootstrap_managed_keys: None
|
||||
) -> None:
|
||||
"""Без строк в БД (все 4 ключа «отсутствуют») `--force` не нужен — пишутся все 4."""
|
||||
_set_bootstrap_env(monkeypatch, chat_enabled=False, ai_enabled=False, ai_level="min")
|
||||
try:
|
||||
written = await apply_preset_settings_module.apply_preset_settings(force=False)
|
||||
assert set(written) == set(BOOTSTRAP_MANAGED_KEYS)
|
||||
|
||||
rows = await _load_managed_rows()
|
||||
assert rows["chat"]["enabled"] is False
|
||||
assert rows["transcriber"]["enabled"] is False
|
||||
assert rows["summarizer"]["enabled"] is False
|
||||
assert rows["ai_level"]["level"] == "min"
|
||||
finally:
|
||||
get_settings.cache_clear()
|
||||
|
||||
|
||||
async def test_apply_preset_settings_without_force_keeps_existing_keys(
|
||||
monkeypatch: pytest.MonkeyPatch, clean_bootstrap_managed_keys: None
|
||||
) -> None:
|
||||
"""Живая инсталляция (строки уже есть, возможно с ручными правками) — без `--force`
|
||||
ничего не перезаписывается, хотя переменные окружения просят другой пресет."""
|
||||
plugins = load_plugins_config(PLUGINS_YAML)
|
||||
async with async_session_maker() as session:
|
||||
session.add(InstanceSetting(key="chat", value={"enabled": False}))
|
||||
session.add(
|
||||
InstanceSetting(key="transcriber", value=plugins.transcriber.model_dump(mode="json"))
|
||||
)
|
||||
session.add(
|
||||
InstanceSetting(key="summarizer", value=plugins.summarizer.model_dump(mode="json"))
|
||||
)
|
||||
session.add(InstanceSetting(key="ai_level", value={"level": "min"}))
|
||||
await session.commit()
|
||||
|
||||
_set_bootstrap_env(monkeypatch, chat_enabled=True, ai_enabled=True, ai_level="max")
|
||||
try:
|
||||
written = await apply_preset_settings_module.apply_preset_settings(force=False)
|
||||
assert written == []
|
||||
|
||||
rows = await _load_managed_rows()
|
||||
assert rows["chat"]["enabled"] is False # ручная правка не тронута
|
||||
assert rows["ai_level"]["level"] == "min"
|
||||
finally:
|
||||
get_settings.cache_clear()
|
||||
|
||||
|
||||
async def test_apply_preset_settings_with_force_overwrites_existing_keys(
|
||||
monkeypatch: pytest.MonkeyPatch, clean_bootstrap_managed_keys: None
|
||||
) -> None:
|
||||
"""`--force` (подтверждение в опроснике install.sh) перезаписывает все 4 ключа."""
|
||||
plugins = load_plugins_config(PLUGINS_YAML)
|
||||
async with async_session_maker() as session:
|
||||
session.add(InstanceSetting(key="chat", value={"enabled": False}))
|
||||
session.add(
|
||||
InstanceSetting(key="transcriber", value=plugins.transcriber.model_dump(mode="json"))
|
||||
)
|
||||
session.add(
|
||||
InstanceSetting(key="summarizer", value=plugins.summarizer.model_dump(mode="json"))
|
||||
)
|
||||
session.add(InstanceSetting(key="ai_level", value={"level": "max"}))
|
||||
await session.commit()
|
||||
|
||||
_set_bootstrap_env(monkeypatch, chat_enabled=True, ai_enabled=False, ai_level="min")
|
||||
try:
|
||||
written = await apply_preset_settings_module.apply_preset_settings(force=True)
|
||||
assert set(written) == set(BOOTSTRAP_MANAGED_KEYS)
|
||||
|
||||
rows = await _load_managed_rows()
|
||||
assert rows["chat"]["enabled"] is True
|
||||
assert rows["transcriber"]["enabled"] is False
|
||||
assert rows["summarizer"]["enabled"] is False
|
||||
assert rows["ai_level"]["level"] == "min"
|
||||
finally:
|
||||
get_settings.cache_clear()
|
||||
Reference in New Issue
Block a user