Files
mrleo1nidandClaude Opus 5 20a666e0d4 Этап 5: окно настроек и сборка exe
- Окно настроек: модель и провайдер (со списком моделей с сервера), озвучка,
  голосовой ввод, слово активации, автозапуск с Windows
- Сохранение в config.yaml через ruamel.yaml с комментариями и ссылками ${VAR},
  проверка значений до записи; переключатели трея тоже сохраняются
- Настройки LLM и переключатели применяются на лету, для остального — перезапуск
- Один экземпляр приложения, лог в файл, аргументы --config и --wait-pid, русские диалоги Qt
- Сборка PyInstaller (onedir) с библиотеками CUDA, иконка приложения
- Тесты сохранения настроек и окна настроек

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 04:10:31 +03:00

86 lines
3.0 KiB
Python

from dataclasses import replace
from pathlib import Path
import pytest
from PySide6.QtWidgets import QApplication
from agr_assistent.config import ConfigError, load_config, save_config_updates
from agr_assistent.core.settings import Settings, needs_restart
from agr_assistent.ui.settings_dialog import SettingsDialog
_USER_CONFIG = """\
# мой конфиг
llm:
provider: ollama # локально
providers:
openrouter:
api_key: ${TEST_SETTINGS_KEY}
"""
@pytest.fixture
def config_path(tmp_path: Path) -> Path:
path = tmp_path / "config.yaml"
path.write_text(_USER_CONFIG, encoding="utf-8")
return path
def test_save_keeps_comments_and_env_references(config_path: Path) -> None:
config = save_config_updates(
config_path, {"llm.provider": "openrouter", "wake_word.phrases": ["эй", "компьютер"]}
)
text = config_path.read_text(encoding="utf-8")
assert "# мой конфиг" in text
assert "provider: openrouter # локально" in text
assert "api_key: ${TEST_SETTINGS_KEY}" in text
assert config.llm.provider == "openrouter"
assert load_config(config_path).wake_word.phrases == ["эй", "компьютер"]
def test_invalid_update_does_not_touch_file(config_path: Path) -> None:
with pytest.raises(ConfigError):
save_config_updates(config_path, {"llm.provider": "nope"})
assert config_path.read_text(encoding="utf-8") == _USER_CONFIG
def test_restart_is_needed_only_for_heavy_settings(config_path: Path) -> None:
config = load_config(config_path)
live = replace(config, tts=replace(config.tts, enabled=False))
live.llm = replace(config.llm, temperature=0.1)
assert not needs_restart(config, live)
assert needs_restart(config, replace(config, voice=replace(config.voice, hotkey="f9")))
assert needs_restart(config, replace(config, tts=replace(config.tts, speaker="aidar")))
def test_dialog_saves_only_changed_values(
qapp: QApplication, config_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setenv("TEST_SETTINGS_KEY", "secret")
settings = Settings(load_config(config_path))
changes: list[object] = []
settings.changed.connect(changes.append)
dialog = SettingsDialog(settings)
# Переключаемся на openrouter: ключ показан ссылкой на переменную, а не секретом
dialog._provider.setCurrentText("openrouter")
assert dialog._api_key.text() == "${TEST_SETTINGS_KEY}"
dialog._model.setEditText("anthropic/claude-sonnet-5")
dialog._tts_enabled.setChecked(False)
assert dialog._collect() == {
"llm.provider": "openrouter",
"llm.providers.openrouter.model": "anthropic/claude-sonnet-5",
"tts.enabled": False,
}
dialog._save()
text = config_path.read_text(encoding="utf-8")
assert "secret" not in text
assert settings.config.llm.active_provider.model == "anthropic/claude-sonnet-5"
assert settings.config.llm.active_provider.api_key == "secret"
assert len(changes) == 1