mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-21 16:18:55 +00:00
Forward explicit catalog refreshes and make the TTS availability gate follow the configured provider instead of unrelated credentials.
226 lines
9.3 KiB
Python
226 lines
9.3 KiB
Python
"""Tests for the Mistral (Voxtral) TTS provider in tools/tts_tool.py."""
|
|
|
|
import base64
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def clean_env(monkeypatch):
|
|
for key in ("MISTRAL_API_KEY", "HERMES_SESSION_PLATFORM"):
|
|
monkeypatch.delenv(key, raising=False)
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_mistral_module():
|
|
mock_client = MagicMock()
|
|
mock_client.__enter__ = MagicMock(return_value=mock_client)
|
|
mock_client.__exit__ = MagicMock(return_value=False)
|
|
mock_mistral_cls = MagicMock(return_value=mock_client)
|
|
fake_module = MagicMock()
|
|
fake_module.Mistral = mock_mistral_cls
|
|
with patch.dict("sys.modules", {"mistralai": fake_module, "mistralai.client": fake_module}):
|
|
yield mock_client
|
|
|
|
|
|
class TestGenerateMistralTts:
|
|
def test_missing_api_key_raises_value_error(self, tmp_path, mock_mistral_module):
|
|
from tools.tts_tool import _generate_mistral_tts
|
|
|
|
output_path = str(tmp_path / "test.mp3")
|
|
with pytest.raises(ValueError, match="MISTRAL_API_KEY"):
|
|
_generate_mistral_tts("Hello", output_path, {})
|
|
|
|
def test_successful_generation(self, tmp_path, mock_mistral_module, monkeypatch):
|
|
from tools.tts_tool import _generate_mistral_tts
|
|
|
|
monkeypatch.setenv("MISTRAL_API_KEY", "test-key")
|
|
audio_content = b"fake-audio-bytes"
|
|
mock_mistral_module.audio.speech.complete.return_value = MagicMock(
|
|
audio_data=base64.b64encode(audio_content).decode()
|
|
)
|
|
|
|
output_path = str(tmp_path / "test.mp3")
|
|
result = _generate_mistral_tts("Hello world", output_path, {})
|
|
|
|
assert result == output_path
|
|
assert (tmp_path / "test.mp3").read_bytes() == audio_content
|
|
mock_mistral_module.audio.speech.complete.assert_called_once()
|
|
mock_mistral_module.__exit__.assert_called_once()
|
|
call_kwargs = mock_mistral_module.audio.speech.complete.call_args[1]
|
|
assert call_kwargs["input"] == "Hello world"
|
|
assert call_kwargs["response_format"] == "mp3"
|
|
|
|
@pytest.mark.parametrize(
|
|
"extension, expected_format",
|
|
[(".ogg", "opus"), (".wav", "wav"), (".flac", "flac"), (".mp3", "mp3")],
|
|
)
|
|
def test_response_format_from_extension(
|
|
self, tmp_path, mock_mistral_module, monkeypatch, extension, expected_format
|
|
):
|
|
from tools.tts_tool import _generate_mistral_tts
|
|
|
|
monkeypatch.setenv("MISTRAL_API_KEY", "test-key")
|
|
mock_mistral_module.audio.speech.complete.return_value = MagicMock(
|
|
audio_data=base64.b64encode(b"data").decode()
|
|
)
|
|
|
|
output_path = str(tmp_path / f"test{extension}")
|
|
_generate_mistral_tts("Hi", output_path, {})
|
|
|
|
call_kwargs = mock_mistral_module.audio.speech.complete.call_args[1]
|
|
assert call_kwargs["response_format"] == expected_format
|
|
|
|
def test_voice_id_passed_when_configured(
|
|
self, tmp_path, mock_mistral_module, monkeypatch
|
|
):
|
|
from tools.tts_tool import _generate_mistral_tts
|
|
|
|
monkeypatch.setenv("MISTRAL_API_KEY", "test-key")
|
|
mock_mistral_module.audio.speech.complete.return_value = MagicMock(
|
|
audio_data=base64.b64encode(b"data").decode()
|
|
)
|
|
|
|
config = {"mistral": {"voice_id": "my-voice-uuid"}}
|
|
_generate_mistral_tts("Hi", str(tmp_path / "test.mp3"), config)
|
|
|
|
call_kwargs = mock_mistral_module.audio.speech.complete.call_args[1]
|
|
assert call_kwargs["voice_id"] == "my-voice-uuid"
|
|
|
|
def test_default_voice_id_when_absent(
|
|
self, tmp_path, mock_mistral_module, monkeypatch
|
|
):
|
|
from tools.tts_tool import DEFAULT_MISTRAL_TTS_VOICE_ID, _generate_mistral_tts
|
|
|
|
monkeypatch.setenv("MISTRAL_API_KEY", "test-key")
|
|
mock_mistral_module.audio.speech.complete.return_value = MagicMock(
|
|
audio_data=base64.b64encode(b"data").decode()
|
|
)
|
|
|
|
_generate_mistral_tts("Hi", str(tmp_path / "test.mp3"), {})
|
|
|
|
call_kwargs = mock_mistral_module.audio.speech.complete.call_args[1]
|
|
assert call_kwargs["voice_id"] == DEFAULT_MISTRAL_TTS_VOICE_ID
|
|
|
|
def test_default_voice_id_when_empty_string(
|
|
self, tmp_path, mock_mistral_module, monkeypatch
|
|
):
|
|
from tools.tts_tool import DEFAULT_MISTRAL_TTS_VOICE_ID, _generate_mistral_tts
|
|
|
|
monkeypatch.setenv("MISTRAL_API_KEY", "test-key")
|
|
mock_mistral_module.audio.speech.complete.return_value = MagicMock(
|
|
audio_data=base64.b64encode(b"data").decode()
|
|
)
|
|
|
|
config = {"mistral": {"voice_id": ""}}
|
|
_generate_mistral_tts("Hi", str(tmp_path / "test.mp3"), config)
|
|
|
|
call_kwargs = mock_mistral_module.audio.speech.complete.call_args[1]
|
|
assert call_kwargs["voice_id"] == DEFAULT_MISTRAL_TTS_VOICE_ID
|
|
|
|
def test_api_error_sanitized(self, tmp_path, mock_mistral_module, monkeypatch):
|
|
from tools.tts_tool import _generate_mistral_tts
|
|
|
|
monkeypatch.setenv("MISTRAL_API_KEY", "test-key")
|
|
mock_mistral_module.audio.speech.complete.side_effect = RuntimeError(
|
|
"secret-key-in-error"
|
|
)
|
|
|
|
with pytest.raises(RuntimeError, match="RuntimeError") as exc_info:
|
|
_generate_mistral_tts("Hello", str(tmp_path / "test.mp3"), {})
|
|
assert "secret-key-in-error" not in str(exc_info.value)
|
|
|
|
def test_default_model_used(self, tmp_path, mock_mistral_module, monkeypatch):
|
|
from tools.tts_tool import DEFAULT_MISTRAL_TTS_MODEL, _generate_mistral_tts
|
|
|
|
monkeypatch.setenv("MISTRAL_API_KEY", "test-key")
|
|
mock_mistral_module.audio.speech.complete.return_value = MagicMock(
|
|
audio_data=base64.b64encode(b"data").decode()
|
|
)
|
|
|
|
_generate_mistral_tts("Hi", str(tmp_path / "test.mp3"), {})
|
|
|
|
call_kwargs = mock_mistral_module.audio.speech.complete.call_args[1]
|
|
assert call_kwargs["model"] == DEFAULT_MISTRAL_TTS_MODEL
|
|
|
|
def test_model_from_config_overrides_default(
|
|
self, tmp_path, mock_mistral_module, monkeypatch
|
|
):
|
|
from tools.tts_tool import _generate_mistral_tts
|
|
|
|
monkeypatch.setenv("MISTRAL_API_KEY", "test-key")
|
|
mock_mistral_module.audio.speech.complete.return_value = MagicMock(
|
|
audio_data=base64.b64encode(b"data").decode()
|
|
)
|
|
|
|
config = {"mistral": {"model": "voxtral-large-tts-9999"}}
|
|
_generate_mistral_tts("Hi", str(tmp_path / "test.mp3"), config)
|
|
|
|
call_kwargs = mock_mistral_module.audio.speech.complete.call_args[1]
|
|
assert call_kwargs["model"] == "voxtral-large-tts-9999"
|
|
|
|
|
|
class TestTtsDispatcherMistral:
|
|
def test_dispatcher_routes_to_mistral(
|
|
self, tmp_path, mock_mistral_module, monkeypatch
|
|
):
|
|
import json
|
|
|
|
from tools.tts_tool import text_to_speech_tool
|
|
|
|
monkeypatch.setenv("MISTRAL_API_KEY", "test-key")
|
|
mock_mistral_module.audio.speech.complete.return_value = MagicMock(
|
|
audio_data=base64.b64encode(b"audio").decode()
|
|
)
|
|
|
|
output_path = str(tmp_path / "out.mp3")
|
|
with patch("tools.tts_tool._load_tts_config", return_value={"provider": "mistral"}):
|
|
result = json.loads(text_to_speech_tool("Hello", output_path=output_path))
|
|
|
|
assert result["success"] is True
|
|
assert result["provider"] == "mistral"
|
|
mock_mistral_module.audio.speech.complete.assert_called_once()
|
|
|
|
def test_dispatcher_returns_error_when_sdk_not_installed(self, tmp_path, monkeypatch):
|
|
import json
|
|
|
|
from tools.tts_tool import text_to_speech_tool
|
|
|
|
monkeypatch.setenv("MISTRAL_API_KEY", "test-key")
|
|
with patch(
|
|
"tools.tts_tool._import_mistral_client", side_effect=ImportError("no module")
|
|
), patch("tools.tts_tool._load_tts_config", return_value={"provider": "mistral"}):
|
|
result = json.loads(
|
|
text_to_speech_tool("Hello", output_path=str(tmp_path / "out.mp3"))
|
|
)
|
|
|
|
assert result["success"] is False
|
|
assert "mistralai" in result["error"]
|
|
|
|
|
|
class TestCheckTtsRequirementsMistral:
|
|
def test_mistral_sdk_and_key_returns_true(self, mock_mistral_module, monkeypatch):
|
|
from tools.tts_tool import check_tts_requirements
|
|
|
|
monkeypatch.setenv("MISTRAL_API_KEY", "test-key")
|
|
with patch(
|
|
"tools.tts_tool._load_tts_config",
|
|
return_value={"provider": "mistral"},
|
|
), patch("tools.tts_tool._import_edge_tts", side_effect=ImportError), \
|
|
patch("tools.tts_tool._import_elevenlabs", side_effect=ImportError), \
|
|
patch("tools.tts_tool._import_openai_client", side_effect=ImportError), \
|
|
patch("tools.tts_tool._check_neutts_available", return_value=False):
|
|
assert check_tts_requirements() is True
|
|
|
|
def test_mistral_key_missing_returns_false(self, mock_mistral_module):
|
|
from tools.tts_tool import check_tts_requirements
|
|
|
|
with patch("tools.tts_tool._import_edge_tts", side_effect=ImportError), \
|
|
patch("tools.tts_tool._import_elevenlabs", side_effect=ImportError), \
|
|
patch("tools.tts_tool._import_openai_client", side_effect=ImportError), \
|
|
patch("tools.tts_tool._check_neutts_available", return_value=False), \
|
|
patch("tools.tts_tool._check_kittentts_available", return_value=False), \
|
|
patch("tools.tts_tool._check_piper_available", return_value=False), \
|
|
patch("tools.tts_tool._has_any_command_tts_provider", return_value=False):
|
|
assert check_tts_requirements() is False
|