fix: send engine=edge from the first-party Edge callers

The settings UI writes localStorage immediately but persists the server
engine through a debounced save, so a first-party Edge selection could
fall through the new server default to a stale ElevenLabs/OpenAI
engine. Both WebUI Edge callers now pass engine explicitly; regressions
cover both call sites.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CNmJZVV9u6pBhcwVhwkPp6
This commit is contained in:
Matt Van HornandClaude Fable 5.1 committed 2026-09-16 12:24:30 -07:00
1 parent a375aa9356
commit cf62bb87bf
3 files changed
+145 -2

No files matched your search

+1 -1
View File
@@ -1901,7 +1901,7 @@ window.renderTranscript=function(container, messages, opts){
fetch(new URL('api/tts', document.baseURI || location.href).href, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({text: clean, voice, rate, pitch})
body: JSON.stringify({text: clean, voice, rate, pitch, engine: 'edge'})
})
.then(r => {
if(!r.ok) throw new Error('TTS request failed: ' + r.status);
+1 -1
View File
@@ -9168,7 +9168,7 @@ function _playEdgeTtsChunked(text, btn){
fetch(new URL('api/tts', document.baseURI || location.href).href, {
method:'POST',
headers:{'Content-Type':'application/json'},
body:JSON.stringify({text:chunk, voice:voice, rate:rate, pitch:pitch})
body:JSON.stringify({text:chunk, voice:voice, rate:rate, pitch:pitch, engine:'edge'})
})
.then(function(r){
if(!r.ok){
@@ -0,0 +1,143 @@
"""G7394-1: first-party Edge callers must send engine:'edge' explicitly.
The Settings selector writes localStorage immediately, then saves the server
setting through a 350ms debounced autosave. During that window (or if the
save fails) the WebUI dispatches the Edge branch while the server still holds
ElevenLabs/OpenAI. Omitting engine lets the persisted-engine fallback silently
play the stale provider.
These tests execute the production ui.js / boot.js callers with a local Edge
selection and assert the /api/tts request body carries the explicit override.
"""
from __future__ import annotations
import json
import shutil
import subprocess
from pathlib import Path
import pytest
from tests.js_source_extract import extract_function
ROOT = Path(__file__).resolve().parents[1]
UI_JS = (ROOT / "static" / "ui.js").read_text(encoding="utf-8")
BOOT_JS = (ROOT / "static" / "boot.js").read_text(encoding="utf-8")
NODE = shutil.which("node")
pytestmark = pytest.mark.skipif(NODE is None, reason="node not on PATH")
def _run_node(script: str) -> list[dict]:
result = subprocess.run(
[NODE, "-e", script],
cwd=ROOT,
capture_output=True,
text=True,
timeout=30,
)
assert result.returncode == 0, (
f"node subprocess failed:\n--- stdout ---\n{result.stdout}\n"
f"--- stderr ---\n{result.stderr}"
)
return json.loads(result.stdout.strip().splitlines()[-1])
_UI_HARNESS = r"""
const requests = [];
const store = {
'hermes-tts-engine': 'edge',
'hermes-tts-voice': 'en-US-AriaNeural',
};
globalThis.window = globalThis;
globalThis.speechSynthesis = { cancel() {}, getVoices() { return []; } };
globalThis.localStorage = {
getItem: (k) => Object.prototype.hasOwnProperty.call(store, k) ? store[k] : null,
};
globalThis.document = {
baseURI: 'http://localhost:8787/',
querySelectorAll: () => [],
};
globalThis.location = { href: 'http://localhost:8787/' };
globalThis.fetch = (url, opts) => {
requests.push({ url: String(url), body: JSON.parse(opts.body) });
return new Promise(() => {});
};
let _ttsSpeaking = false;
let _playingEdgeAudio = null;
let _ttsCurrentUtterance = null;
let _ttsChunkQueue = [];
let _ttsChunkIndex = 0;
let _ttsActiveBtn = null;
__UI_FNS__
const row = { dataset: { rawText: 'Hello from Edge' } };
const btn = { dataset: { speaking: '0' }, closest: () => row };
speakMessage(btn);
console.log(JSON.stringify(requests));
"""
_BOOT_HARNESS = r"""
const requests = [];
const store = {
'hermes-tts-engine': 'edge',
'hermes-tts-voice': 'en-US-AriaNeural',
};
globalThis.window = globalThis;
globalThis.localStorage = {
getItem: (k) => Object.prototype.hasOwnProperty.call(store, k) ? store[k] : null,
};
globalThis.document = {
baseURI: 'http://localhost:8787/',
querySelectorAll: () => [{ dataset: { rawText: 'Hello from Edge' } }],
};
globalThis.location = { href: 'http://localhost:8787/' };
globalThis.fetch = (url, opts) => {
requests.push({ url: String(url), body: JSON.parse(opts.body) });
return new Promise(() => {});
};
const S = { session: { session_id: 'sid-edge' } };
let _voiceModeActive = true;
let _voiceModeThinkingSid = null;
let _ttsSpeaking = false;
let _playingEdgeAudio = null;
function _setState() {}
function _startListening() {}
__BOOT_FNS__
_speakResponse();
console.log(JSON.stringify(requests));
"""
def test_play_edge_tts_chunked_sends_explicit_engine_when_server_still_elevenlabs():
"""Local Edge selection (Listen button) must override persisted ElevenLabs."""
fns = "\n".join(
[
extract_function(UI_JS, "_stripForTTS"),
extract_function(UI_JS, "_splitForTTS"),
extract_function(UI_JS, "_playEdgeTtsChunked"),
extract_function(UI_JS, "stopTTS"),
extract_function(UI_JS, "speakMessage"),
]
)
requests = _run_node(_UI_HARNESS.replace("__UI_FNS__", fns))
assert len(requests) == 1
assert requests[0]["url"].endswith("/api/tts")
assert requests[0]["body"]["engine"] == "edge"
assert requests[0]["body"]["text"] == "Hello from Edge"
assert requests[0]["body"]["voice"] == "en-US-AriaNeural"
def test_voice_mode_edge_branch_sends_explicit_engine_when_server_still_openai():
"""Local Edge selection (voice-mode auto-read) must override persisted OpenAI."""
fns = extract_function(BOOT_JS, "_speakResponse")
requests = _run_node(_BOOT_HARNESS.replace("__BOOT_FNS__", fns))
assert len(requests) == 1
assert requests[0]["url"].endswith("/api/tts")
assert requests[0]["body"]["engine"] == "edge"
assert requests[0]["body"]["text"] == "Hello from Edge"
assert requests[0]["body"]["voice"] == "en-US-AriaNeural"