/
/
/
1"""Tests for the TTS engine query helpers."""
2
3from __future__ import annotations
4
5from unittest.mock import AsyncMock, MagicMock
6
7import pytest
8from music_assistant_models.errors import MusicAssistantError
9
10from music_assistant.helpers.tts import (
11 TTSLanguageNotSupportedError,
12 query_tts_engine_with_language_fallback,
13)
14from music_assistant.models.plugin import PluginProvider, TTSEngine
15
16
17def _create_engine() -> tuple[TTSEngine, AsyncMock]:
18 """Create a TTS engine and the mock that renders its speech."""
19 provider = MagicMock(spec=PluginProvider)
20 provider.instance_id = "hass"
21 provider.get_tts_message = AsyncMock()
22 return TTSEngine(id="tts.x", name="X", provider=provider), provider.get_tts_message
23
24
25async def test_language_rejection_retries_without_language(
26 caplog: pytest.LogCaptureFixture,
27) -> None:
28 """A rejected language is retried once with the engine's default voice."""
29 engine, get_tts_message = _create_engine()
30 sentinel = MagicMock()
31 get_tts_message.side_effect = [
32 TTSLanguageNotSupportedError("TTS engine 'tts.x' does not support language 'en-US'"),
33 sentinel,
34 ]
35
36 with caplog.at_level("WARNING"):
37 result = await query_tts_engine_with_language_fallback(engine, "Hello", "en-US")
38
39 assert result is sentinel
40 assert get_tts_message.call_count == 2
41 second_call = get_tts_message.call_args_list[1]
42 assert second_call.kwargs["language"] is None
43 assert "retrying with the engine's default voice" in caplog.text
44
45
46async def test_generic_errors_do_not_retry() -> None:
47 """A generic exception from the engine propagates without a retry."""
48 engine, get_tts_message = _create_engine()
49 get_tts_message.side_effect = RuntimeError("boom")
50
51 with pytest.raises(RuntimeError):
52 await query_tts_engine_with_language_fallback(engine, "Hello", "en-US")
53
54 assert get_tts_message.call_count == 1
55
56
57async def test_music_assistant_error_does_not_retry() -> None:
58 """A structured MusicAssistantError propagates without a retry."""
59 engine, get_tts_message = _create_engine()
60 get_tts_message.side_effect = MusicAssistantError("nope")
61
62 with pytest.raises(MusicAssistantError):
63 await query_tts_engine_with_language_fallback(engine, "Hello", "en-US")
64
65 assert get_tts_message.call_count == 1
66
67
68async def test_rejection_without_language_does_not_retry() -> None:
69 """A language rejection raised while no language was requested is not retried."""
70 engine, get_tts_message = _create_engine()
71 get_tts_message.side_effect = TTSLanguageNotSupportedError(
72 "TTS engine 'tts.x' does not support language 'en-US'"
73 )
74
75 with pytest.raises(TTSLanguageNotSupportedError):
76 await query_tts_engine_with_language_fallback(engine, "Hello", None)
77
78 assert get_tts_message.call_count == 1
79