fix(#5435): keep saved default speech prefs authoritative

This commit is contained in:
Rod Boev 2026-07-02 16:49:45 -04:00
parent 7ee2757afe
commit ca31bf969b
3 changed files with 65 additions and 25 deletions

View file

@ -2744,22 +2744,23 @@ function _mirrorSpeechSettingsFromServer(s){
voice_silence_ms:1800,
raw_audio_mode:false,
};
const hasServerValue=(settingKey)=>Object.prototype.hasOwnProperty.call(s,settingKey);
const cachedValue=(storageKey)=>{
try{return localStorage.getItem(storageKey);}catch(_){return null;}
};
const boolValue=(value)=>value===true||value==='true';
const resolveBool=(settingKey,storageKey)=>{
const server=Object.prototype.hasOwnProperty.call(s,settingKey)?s[settingKey]:defaults[settingKey];
const server=hasServerValue(settingKey)?s[settingKey]:defaults[settingKey];
const cached=cachedValue(storageKey);
if(cached!==null&&boolValue(server)===boolValue(defaults[settingKey])&&boolValue(cached)!==boolValue(defaults[settingKey])){
if(!hasServerValue(settingKey)&&cached!==null){
return boolValue(cached);
}
return boolValue(server);
};
const resolveScalar=(settingKey,storageKey)=>{
const server=Object.prototype.hasOwnProperty.call(s,settingKey)?s[settingKey]:defaults[settingKey];
const server=hasServerValue(settingKey)?s[settingKey]:defaults[settingKey];
const cached=cachedValue(storageKey);
if(cached!==null&&String(server)===String(defaults[settingKey])&&String(cached)!==String(defaults[settingKey])){
if(!hasServerValue(settingKey)&&cached!==null){
return cached;
}
return server;
@ -2771,7 +2772,7 @@ function _mirrorSpeechSettingsFromServer(s){
['voice_continuous','hermes-voice-continuous'],
];
boolKeys.forEach(([settingKey,storageKey])=>{
if(Object.prototype.hasOwnProperty.call(s,settingKey)){
if(hasServerValue(settingKey)){
try{localStorage.setItem(storageKey,resolveBool(settingKey,storageKey)?'true':'false');}catch(_){}
}
});
@ -2782,11 +2783,11 @@ function _mirrorSpeechSettingsFromServer(s){
['tts_pitch','hermes-tts-pitch'],
['voice_silence_ms','hermes-voice-silence-ms'],
].forEach(([settingKey,storageKey])=>{
if(Object.prototype.hasOwnProperty.call(s,settingKey)){
if(hasServerValue(settingKey)){
try{localStorage.setItem(storageKey,String(resolveScalar(settingKey,storageKey)));}catch(_){}
}
});
if(Object.prototype.hasOwnProperty.call(s,'raw_audio_mode')){
if(hasServerValue('raw_audio_mode')){
const rawAudioMode=resolveBool('raw_audio_mode','hermes-raw-audio-mode');
if(typeof window._applyRawAudioModePreference==='function'){
window._applyRawAudioModePreference(rawAudioMode);

View file

@ -8640,20 +8640,7 @@ async function loadSettingsPanel(){
if(typeof window._mirrorSpeechSettingsFromServer==='function') window._mirrorSpeechSettingsFromServer(settings);
const _speechSetting=function(key,storageKey,fallback,kind){
const stored=localStorage.getItem(storageKey);
if(settings&&Object.prototype.hasOwnProperty.call(settings,key)){
const server=settings[key];
if(stored!==null){
if(kind==='bool'){
const serverBool=server===true||server==='true';
const fallbackBool=fallback===true||fallback==='true';
const storedBool=stored==='true';
if(serverBool===fallbackBool&&storedBool!==fallbackBool) return storedBool;
}else if(String(server)===String(fallback)&&String(stored)!==String(fallback)){
return stored;
}
}
return server;
}
if(settings&&Object.prototype.hasOwnProperty.call(settings,key)) return settings[key];
return stored===null?fallback:stored;
};
const _speechBool=function(key,storageKey,fallback){

View file

@ -45,6 +45,24 @@ def post(path, body=None):
return json.loads(exc.read()), exc.code
def _extract_balanced_block(src, marker):
start = src.index(marker)
brace = src.index("{", start)
depth = 0
end = None
for idx in range(brace, len(src)):
ch = src[idx]
if ch == "{":
depth += 1
elif ch == "}":
depth -= 1
if depth == 0:
end = idx + 1
break
assert end is not None, f"Unbalanced block for {marker!r}"
return src[start:end]
def _reset_speech_settings(extra=None):
payload = dict(SPEECH_DEFAULTS)
if extra:
@ -151,8 +169,8 @@ def test_boot_mirrors_server_settings_before_tts_apply_and_preserves_failure_fal
assert success_call_idx < apply_idx
assert catch_idx < failure_apply_idx
assert "const defaults={" in BOOT_JS
assert "cached!==null&&boolValue(server)===boolValue(defaults[settingKey])" in BOOT_JS
assert "String(server)===String(defaults[settingKey])" in BOOT_JS
assert "const hasServerValue=(settingKey)=>Object.prototype.hasOwnProperty.call(s,settingKey);" in BOOT_JS
assert "if(!hasServerValue(settingKey)&&cached!==null)" in BOOT_JS
for storage_key in [
"hermes-tts-enabled",
"hermes-tts-auto-read",
@ -169,6 +187,41 @@ def test_boot_mirrors_server_settings_before_tts_apply_and_preserves_failure_fal
assert "window._applyRawAudioModePreference" in BOOT_JS
def test_server_saved_defaults_win_over_stale_local_cache_in_boot_and_panel_logic():
mirror_fn = _extract_balanced_block(BOOT_JS, "function _mirrorSpeechSettingsFromServer")
speech_setting_start = PANELS_JS.index("const _speechSetting=function(")
speech_setting_end = PANELS_JS.index("const _speechBool=function", speech_setting_start)
speech_setting_block = PANELS_JS[speech_setting_start:speech_setting_end].strip()
script = f"""
const assert = require('assert');
const localStorage = {{
store: new Map([
['hermes-tts-enabled', 'true'],
['hermes-tts-pitch', '0.8'],
]),
getItem(key) {{
return this.store.has(key) ? this.store.get(key) : null;
}},
setItem(key, value) {{
this.store.set(key, String(value));
}},
}};
const window = {{}};
{mirror_fn}
_mirrorSpeechSettingsFromServer({{tts_enabled: false, tts_pitch: 1}});
assert.strictEqual(localStorage.getItem('hermes-tts-enabled'), 'false');
assert.strictEqual(localStorage.getItem('hermes-tts-pitch'), '1');
let settings = {{tts_enabled: false, tts_pitch: 1}};
{speech_setting_block}
assert.strictEqual(_speechSetting('tts_enabled', 'hermes-tts-enabled', false, 'bool'), false);
assert.strictEqual(_speechSetting('tts_pitch', 'hermes-tts-pitch', 1), 1);
"""
import subprocess
subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True)
def test_settings_panel_persists_speech_fields_and_keeps_immediate_cache_writes():
payload_idx = PANELS_JS.index("function _preferencesPayloadFromUi")
payload_end = PANELS_JS.index("function _setPreferencesAutosaveStatus", payload_idx)
@ -197,8 +250,7 @@ def test_settings_panel_persists_speech_fields_and_keeps_immediate_cache_writes(
assert "savedPitch||'1'" not in panel_block
assert "ttsRateSlider.value=(savedRate===null||savedRate===undefined)?'1':String(savedRate)" in panel_block
assert "ttsPitchSlider.value=(savedPitch===null||savedPitch===undefined)?'1':String(savedPitch)" in panel_block
assert "serverBool===fallbackBool&&storedBool!==fallbackBool" in PANELS_JS
assert "String(server)===String(fallback)&&String(stored)!==String(fallback)" in PANELS_JS
assert "if(settings&&Object.prototype.hasOwnProperty.call(settings,key)) return settings[key];" in PANELS_JS
assert "_schedulePreferencesAutosave()" in panel_block
assert "_applyVoiceModePref" in panel_block
assert "_populateTtsVoices" in panel_block