From fdacb4b92203833d790fae0726bf1356ea048a65 Mon Sep 17 00:00:00 2001 From: Rod Boev Date: Thu, 2 Jul 2026 21:28:46 -0400 Subject: [PATCH] fix(#5435): stop startup workspace repair from persisting speech defaults --- api/config.py | 35 ++++++++++++------- tests/test_issue5435_tts_voice_preferences.py | 34 ++++++++++++++++++ 2 files changed, 57 insertions(+), 12 deletions(-) diff --git a/api/config.py b/api/config.py index cff4fe95..639f95bc 100644 --- a/api/config.py +++ b/api/config.py @@ -8194,6 +8194,18 @@ def persisted_speech_settings_keys() -> list[str]: return sorted(_extract_persisted_speech_keys(_read_raw_settings_file())) +def _settings_payload_for_write(settings: dict, persisted_speech_keys: set[str]) -> dict: + persisted = { + k: v + for k, v in settings.items() + if k not in {"default_model", _SETTINGS_PERSISTED_SPEECH_KEYS_FIELD} + } + for speech_key in _SETTINGS_SPEECH_KEYS: + if speech_key not in persisted_speech_keys: + persisted.pop(speech_key, None) + return persisted + + def load_settings() -> dict: """Load settings from disk, merging with defaults for any missing keys.""" settings = dict(_SETTINGS_DEFAULTS) @@ -8515,17 +8527,7 @@ def save_settings(settings: dict) -> dict: ) effective_persisted_speech_keys = set(persisted_speech_keys) effective_persisted_speech_keys.update(applied_speech_keys) - persisted = { - k: v - for k, v in current.items() - if k not in {"default_model", _SETTINGS_PERSISTED_SPEECH_KEYS_FIELD} - } - # Persist speech keys only if they were already present on disk or explicitly - # submitted in this request. This avoids auto-materializing defaults during - # unrelated saves. - for speech_key in _SETTINGS_SPEECH_KEYS: - if speech_key not in effective_persisted_speech_keys: - persisted.pop(speech_key, None) + persisted = _settings_payload_for_write(current, effective_persisted_speech_keys) SETTINGS_FILE.parent.mkdir(parents=True, exist_ok=True) SETTINGS_FILE.write_text( json.dumps(persisted, ensure_ascii=False, indent=2), @@ -8567,8 +8569,17 @@ if _settings_file_exists: if _startup_settings.get("default_workspace") != str(DEFAULT_WORKSPACE): _startup_settings["default_workspace"] = str(DEFAULT_WORKSPACE) try: + startup_persisted_speech_keys = _extract_persisted_speech_keys( + _read_raw_settings_file() + ) SETTINGS_FILE.write_text( - json.dumps(_startup_settings, ensure_ascii=False, indent=2), + json.dumps( + _settings_payload_for_write( + _startup_settings, startup_persisted_speech_keys + ), + ensure_ascii=False, + indent=2, + ), encoding="utf-8", ) except Exception: diff --git a/tests/test_issue5435_tts_voice_preferences.py b/tests/test_issue5435_tts_voice_preferences.py index 3c5782a1..a44e59f6 100644 --- a/tests/test_issue5435_tts_voice_preferences.py +++ b/tests/test_issue5435_tts_voice_preferences.py @@ -187,6 +187,40 @@ def test_unrelated_settings_save_does_not_materialize_absent_speech_defaults(): _restore_settings_file(path, original) +def test_startup_workspace_repair_write_drops_merged_speech_defaults(): + path, original = _settings_file_snapshot() + cfg = importlib.import_module("api.config") + try: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text( + json.dumps( + { + "show_tps": False, + "tts_pitch": 0.0, + "default_workspace": "C:/stale/workspace", + }, + ensure_ascii=False, + indent=2, + ), + encoding="utf-8", + ) + merged = cfg.load_settings() + merged["default_workspace"] = "C:/fixed/workspace" + persisted = cfg._settings_payload_for_write( + merged, cfg._extract_persisted_speech_keys(cfg._read_raw_settings_file()) + ) + + assert persisted["show_tps"] is False + assert persisted["tts_pitch"] == 0.0 + assert persisted["default_workspace"] == "C:/fixed/workspace" + assert PERSISTED_SPEECH_KEYS_FIELD not in persisted + for key in SPEECH_DEFAULTS: + if key != "tts_pitch": + assert key not in persisted + finally: + _restore_settings_file(path, original) + + def test_invalid_speech_settings_preserve_previous_values_and_unrelated_settings(): data, status = get("/api/settings") original_show_tps = bool(data.get("show_tps"))