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