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
144 lines
4.6 KiB
Python
144 lines
4.6 KiB
Python
"""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"
|