- Окно настроек: модель и провайдер (со списком моделей с сервера), озвучка,
голосовой ввод, слово активации, автозапуск с Windows
- Сохранение в config.yaml через ruamel.yaml с комментариями и ссылками ${VAR},
проверка значений до записи; переключатели трея тоже сохраняются
- Настройки LLM и переключатели применяются на лету, для остального — перезапуск
- Один экземпляр приложения, лог в файл, аргументы --config и --wait-pid, русские диалоги Qt
- Сборка PyInstaller (onedir) с библиотеками CUDA, иконка приложения
- Тесты сохранения настроек и окна настроек
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
86 lines
3.0 KiB
Python
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
|