Skip to main content
Glama

Voice Mode

by mbailey
test_tts_stability.py•12.1 kB
#!/usr/bin/env python """ Test TTS stability and HTTP client lifecycle. These tests are based on the debug script that identified hanging issues. They test: - Multiple TTS cycles without hanging - Proper HTTP client cleanup - Connection pooling behavior - Memory management """ import asyncio import os import tempfile from pathlib import Path from unittest.mock import AsyncMock, MagicMock, patch, Mock import pytest import httpx # Set required environment variables before imports os.environ['OPENAI_API_KEY'] = os.environ.get('OPENAI_API_KEY', 'test-key') # Import from the core module from voice_mode.core import text_to_speech, get_openai_clients, cleanup, save_debug_file class TestTTSStability: """Test TTS stability under repeated use""" @pytest.fixture def mock_tts_response(self): """Create a mock TTS response""" response = MagicMock() response.content = b"fake audio data" * 1000 # ~30KB of fake audio response.read = AsyncMock(return_value=response.content) response.__aenter__ = AsyncMock(return_value=response) response.__aexit__ = AsyncMock(return_value=None) return response @pytest.fixture def mock_openai_client(self, mock_tts_response): """Create a mock OpenAI client with proper async context manager""" client = MagicMock() # Create a proper async context manager mock async def mock_create(*args, **kwargs): # Return the mock response that has __aenter__ and __aexit__ return mock_tts_response # Create a mock streaming response handler streaming_handler = MagicMock() streaming_handler.create = mock_create # Set up the client structure client.audio.speech.with_streaming_response = streaming_handler # Add HTTP client for cleanup testing http_client = AsyncMock() http_client.aclose = AsyncMock() client._client = http_client return client @pytest.mark.skip(reason="Need to refactor for lazy imports") @pytest.mark.asyncio async def test_multiple_tts_cycles(self, mock_openai_client): """Test multiple TTS cycles to ensure no hanging""" # Create mock clients dict openai_clients = {'tts': mock_openai_client} # Mock dependencies with patch('voice_mode.core.AudioSegment') as mock_audio, \ patch('voice_mode.core.logger') as mock_logger: # Mock audio processing mock_audio_instance = MagicMock() mock_audio_instance.channels = 1 mock_audio_instance.frame_rate = 24000 mock_audio_instance.get_array_of_samples.return_value = [0] * 1000 mock_audio.from_mp3.return_value = mock_audio_instance # Mock sounddevice mock_sd.play = MagicMock() mock_sd.wait = MagicMock() # Run multiple cycles for i in range(5): result, metrics = await text_to_speech( text=f"Test message number {i+1}", openai_clients=openai_clients, tts_model="tts-1", tts_voice="nova", tts_base_url="https://api.openai.com/v1", debug=False ) assert result is True assert metrics is not None assert 'generation' in metrics assert 'playback' in metrics # Verify TTS was called assert mock_openai_client.audio.speech.with_streaming_response.create.called # Reset for next cycle mock_openai_client.audio.speech.with_streaming_response.create.reset_mock() # Verify no resource leaks (all calls completed) assert mock_openai_client.audio.speech.with_streaming_response.create.call_count == 0 @pytest.mark.asyncio async def test_http_client_cleanup(self, mock_openai_client): """Test that HTTP clients are properly cleaned up""" # Set up mock clients openai_clients = { 'stt': mock_openai_client, 'tts': mock_openai_client } # Run cleanup await cleanup(openai_clients) # Verify HTTP clients were closed assert mock_openai_client._client.aclose.called assert mock_openai_client._client.aclose.call_count == 2 # Once for STT, once for TTS @pytest.mark.skip(reason="httpx.Timeout API changed") @pytest.mark.asyncio async def test_tts_with_connection_pooling(self): """Test TTS with connection pooling configuration""" with patch('voice_mode.core.AsyncOpenAI') as mock_openai_class, \ patch('voice_mode.core.httpx.AsyncClient') as mock_http_client: # Call get_openai_clients clients = get_openai_clients( api_key="test-key", stt_base_url="https://api.openai.com/v1", tts_base_url="https://api.openai.com/v1" ) # Verify HTTP client was created with proper config mock_http_client.assert_called() call_args = mock_http_client.call_args[1] # Check timeout configuration assert isinstance(call_args['timeout'], httpx.Timeout) assert call_args['timeout'].timeout == 30.0 assert call_args['timeout'].connect == 5.0 # Check connection limits assert isinstance(call_args['limits'], httpx.Limits) assert call_args['limits'].max_keepalive_connections == 5 assert call_args['limits'].max_connections == 10 @pytest.mark.skip(reason="Complex async context manager mocking - error handling tested elsewhere") @pytest.mark.asyncio async def test_tts_error_handling(self): """Test TTS error handling and recovery""" # This test is difficult to mock properly because text_to_speech uses async context managers # Error handling is tested in the converse critical path tests instead pass @pytest.mark.skip(reason="Need to refactor for lazy imports") @pytest.mark.asyncio async def test_debug_logging(self, mock_openai_client, tmp_path): """Test debug logging functionality""" # Set debug mode debug_dir = tmp_path / "voice-mode_recordings" debug_dir.mkdir(exist_ok=True) # Test saving debug file test_data = b"test audio data" debug_path = save_debug_file(test_data, "test", "mp3", debug_dir, debug=True) assert debug_path is not None assert Path(debug_path).exists() assert Path(debug_path).read_bytes() == test_data # Test debug mode with TTS openai_clients = {'tts': mock_openai_client} with patch('voice_mode.core.AudioSegment') as mock_audio, \ patch('voice_mode.core.sd') as mock_sd: # Mock audio processing mock_audio_instance = MagicMock() mock_audio_instance.channels = 1 mock_audio_instance.frame_rate = 24000 mock_audio_instance.get_array_of_samples.return_value = [0] * 1000 mock_audio.from_mp3.return_value = mock_audio_instance # Mock sounddevice mock_sd.play = MagicMock() mock_sd.wait = MagicMock() # Test with debug enabled result, metrics = await text_to_speech( text="Debug test", openai_clients=openai_clients, tts_model="tts-1", tts_voice="nova", tts_base_url="https://api.openai.com/v1", debug=True, debug_dir=debug_dir ) assert result is True assert metrics is not None @pytest.mark.asyncio async def test_trace_logging(self, tmp_path): """Test trace logging functionality""" os.environ['VOICE_MODE_DEBUG'] = 'trace' trace_file = tmp_path / "voice_mode_trace.log" # This test would require actually importing the module with trace enabled # For now, we'll test the trace file creation logic assert os.environ['VOICE_MODE_DEBUG'] == 'trace' # In actual implementation, trace logging would create the file # We'll just verify the environment is set correctly assert os.getenv('VOICE_MODE_DEBUG', '').lower() == 'trace' class TestMemoryManagement: """Test memory management and garbage collection""" @pytest.mark.asyncio async def test_garbage_collection_on_cleanup(self): """Test that garbage collection runs during cleanup""" with patch('voice_mode.core.gc') as mock_gc: mock_gc.collect.return_value = 42 # Number of objects collected await cleanup({}) # Verify garbage collection was called mock_gc.collect.assert_called_once() class TestAudioFileHandling: """Test audio file creation and cleanup""" @pytest.mark.skip(reason="Missing fixture - need to refactor") @pytest.mark.asyncio async def test_temporary_file_cleanup(self, mock_openai_client): """Test that temporary files are cleaned up""" openai_clients = {'tts': mock_openai_client} temp_files_created = [] original_tempfile = tempfile.NamedTemporaryFile def track_tempfile(*args, **kwargs): """Track temporary files created""" tmp = original_tempfile(*args, **kwargs) temp_files_created.append(tmp.name) return tmp with patch('voice_mode.core.AudioSegment') as mock_audio, \ patch('voice_mode.core.sd') as mock_sd, \ patch('voice_mode.core.tempfile.NamedTemporaryFile', track_tempfile), \ patch('voice_mode.core.os.unlink') as mock_unlink: # Mock audio processing mock_audio_instance = MagicMock() mock_audio_instance.channels = 1 mock_audio_instance.frame_rate = 24000 mock_audio_instance.get_array_of_samples.return_value = [0] * 1000 mock_audio.from_mp3.return_value = mock_audio_instance # Mock sounddevice mock_sd.play = MagicMock() mock_sd.wait = MagicMock() result, metrics = await text_to_speech( text="Test", openai_clients=openai_clients, tts_model="tts-1", tts_voice="nova", tts_base_url="https://api.openai.com/v1", debug=False ) # Verify temporary file was created and cleaned up assert len(temp_files_created) > 0 mock_unlink.assert_called() # Run specific debug scenario tests @pytest.mark.asyncio async def test_debug_scenario_reproduction(): """Reproduce the exact scenario from test_voice_debug.py""" from openai import AsyncOpenAI # This test requires actual API key to fully reproduce api_key = os.getenv("OPENAI_API_KEY") if not api_key or api_key == "test-key": pytest.skip("Requires real OPENAI_API_KEY for integration test") client = AsyncOpenAI(api_key=api_key) try: # Test a single cycle (not 5 to avoid rate limits in tests) response = await client.audio.speech.create( model="tts-1", input="Test message", voice="nova", response_format="mp3" ) assert len(response.content) > 0 # Clean up await client.close() except Exception as e: pytest.skip(f"API test failed: {e}")

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/mbailey/voicemode'

If you have feedback or need assistance with the MCP directory API, please join our Discord server