Add a --deep probe to check_tts so a live TTS failure isn't invisible

Reported: the pet was speaking in the offline pyttsx3 voice instead of the
server-synthesized one. speak()'s fallback chain is silent by design (a
TtsError just logs and falls through to pyttsx3), and the shallow doctor
check only ever verified config presence, not that a real /desk/tts call
actually succeeds — so "misconfigured" and "configured but the server
rejects it" looked identical from the outside.

check_tts(deep=True) now posts a couple of words to /desk/tts for real and
reports the exact HTTP status and server error message (a 404 means the
voice id isn't real or isn't owned by this key - the likely shape of a
Voicebox-clone ownership mismatch; 402 means the account is out of
credits; 401 means DESK_API_KEY is wrong). Wired through the same
CHECKS/run() deep-parameter dance check_streaming_endpoint already uses.

5 new tests, including one pinning that run()'s TypeError-fallback
actually reaches check_tts's deep parameter rather than silently running
it shallow.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015CBvympe6SuHdhf9q1VQqS
This commit is contained in:
2026-09-13 16:35:01 -06:00
co-authored by Claude Sonnet 5
parent 2a2cf38399
commit ad9925da02
2 changed files with 121 additions and 10 deletions
+73
View File
@@ -9,6 +9,7 @@ pinning down.
import sys
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
@@ -112,3 +113,75 @@ def test_a_slow_silence_timeout_is_flagged(monkeypatch):
def test_a_check_line_renders_the_fix_only_when_there_is_a_problem():
assert "" not in doctor.Check("x", OK, "all good", fix="unused").line()
assert "→ do the thing" in doctor.Check("x", WARN, "hmm", fix="do the thing").line()
# ── check_tts's --deep probe ────────────────────────────────────────────────
# check_streaming_endpoint already established the pattern: shallow only
# checks config (no network), --deep actually calls the server. TTS needs
# its own deep probe specifically because "configured but broken" (a
# rejected voice id, a dead credit balance) is otherwise indistinguishable
# from "working" until speak() silently falls back to the offline voice —
# which is exactly the bug report this was added to diagnose.
@pytest.fixture(autouse=True)
def _tts_configured(monkeypatch):
monkeypatch.setattr(config, "SERVER_URL", "http://test-server:5002")
monkeypatch.setattr(config, "API_KEY", "test-key")
monkeypatch.setattr(config, "SESSION_ID", "pet-test")
monkeypatch.setattr(config, "ELEVENLABS_VOICE_ID", "abc123voiceid")
def test_tts_shallow_does_not_touch_the_network():
with patch("requests.post") as post:
check = doctor.check_tts(deep=False)
post.assert_not_called()
assert check.status == OK
assert "not probed" in check.detail
def test_tts_deep_reports_success():
response = MagicMock(status_code=200, content=b"\x00\x01" * 100)
with patch("requests.post", return_value=response) as post:
check = doctor.check_tts(deep=True)
post.assert_called_once()
assert check.status == OK
assert "200 bytes" in check.detail
args, kwargs = post.call_args
assert args[0] == "http://test-server:5002/desk/tts"
assert kwargs["headers"] == {"X-Desk-Api-Key": "test-key"}
assert kwargs["json"]["voice_id"] == "abc123voiceid"
def test_tts_deep_surfaces_the_servers_error_message():
"""A 404 with 'voice not found' is exactly the shape of the reported bug:
a vb: clone owned by someone else, or a stale/mistyped id."""
response = MagicMock(status_code=404)
response.json.return_value = {"error": "voice not found"}
with patch("requests.post", return_value=response):
check = doctor.check_tts(deep=True)
assert check.status == FAIL
assert "404" in check.detail
assert "voice not found" in check.detail
assert check.fix
def test_tts_deep_handles_a_network_failure_without_raising():
with patch("requests.post", side_effect=OSError("no route to host")):
check = doctor.check_tts(deep=True)
assert check.status == FAIL
assert "no route to host" in check.detail
def test_tts_run_wires_deep_through_automatically(monkeypatch):
"""run()'s TypeError-fallback dance (doctor.py's CHECKS loop) must
actually reach check_tts's deep parameter, not silently call it shallow."""
seen = []
def fake_check_tts(deep=False):
seen.append(deep)
return doctor.Check("text-to-speech", OK, "stub")
monkeypatch.setattr(doctor, "CHECKS", (("tts", fake_check_tts),))
doctor.run(deep=True)
assert seen == [True]