"""Unit tests for the TTS adapters (TASK-02-02). Both adapters must pass with a mock stream and degrade gracefully when keys / voice models are absent. PRAXIS_TTS selection must route to the right adapter. """ from __future__ import annotations import asyncio from unittest.mock import patch import pytest from server.services.base import TTSProvider, TTSResult from server.tts.cartesia_tts import CartesiaTTS from server.tts.piper_tts import PiperTTS def test_cartesia_selectable_via_env(monkeypatch): """PRAXIS_TTS=cartesia selects CartesiaTTS.""" from server.services.registry import get_tts monkeypatch.setenv("PRAXIS_TTS", "cartesia") monkeypatch.setenv("CARTESIA_API_KEY", "test-key") get_tts.cache_clear() tts = get_tts() assert isinstance(tts, CartesiaTTS) assert tts.name == "cartesia" assert tts.voice_id # has a default voice id def test_piper_selectable_via_env(monkeypatch): """PRAXIS_TTS=piper selects PiperTTS.""" from server.services.registry import get_tts monkeypatch.setenv("PRAXIS_TTS", "piper") get_tts.cache_clear() tts = get_tts() assert isinstance(tts, PiperTTS) assert tts.name == "piper" def test_cartesia_missing_key_no_audio(): """Cartesia with no API key yields no audio but doesn't crash (graceful).""" tts = CartesiaTTS(api_key="") async def _run(): chunks = [c async for c in tts.synthesize("hello")] return chunks chunks = asyncio.run(_run()) assert chunks == [] def test_cartesia_synthesize_all_with_mock(monkeypatch): """Cartesia.synthesize_all returns audio + TTSResult with a mocked stream.""" tts = CartesiaTTS(api_key="test-key", voice_id="v1") async def _fake_stream(text): yield b"\x00\x01" yield b"\x02\x03" monkeypatch.setattr(tts, "synthesize", _fake_stream) audio, result = asyncio.run(tts.synthesize_all("hi")) assert audio == b"\x00\x01\x02\x03" assert result.chars == 2 assert result.voice_id == "v1" assert result.first_audio_ms is not None def test_piper_missing_model_no_audio(): """Piper with no voice model yields no audio but doesn't crash (graceful).""" tts = PiperTTS(voice_model="") async def _run(): chunks = [c async for c in tts.synthesize("hello")] return chunks chunks = asyncio.run(_run()) assert chunks == [] def test_piper_synthesize_all_with_mock(monkeypatch): """Piper.synthesize_all returns audio + TTSResult with a mocked stream.""" tts = PiperTTS(voice_model="/nonexistent", voice_id="p1") async def _fake_stream(text): yield b"\x10\x20" yield b"\x30\x40" monkeypatch.setattr(tts, "synthesize", _fake_stream) audio, result = asyncio.run(tts.synthesize_all("hi")) assert audio == b"\x10\x20\x30\x40" assert result.chars == 2 assert result.voice_id == "p1" def test_both_adapters_are_ttsprovider(): """Both adapters satisfy the TTSProvider ABC.""" assert isinstance(CartesiaTTS(api_key="k"), TTSProvider) assert isinstance(PiperTTS(), TTSProvider)