Skip to main content
Glama

Voice Mode

by mbailey
test_stt_error_handling.py•10.6 kB
"""Tests for STT error handling and connection failure detection""" import pytest from unittest.mock import AsyncMock, MagicMock, patch, mock_open from openai import NotFoundError, AuthenticationError, APIConnectionError, OpenAIError from httpx import Response, Request import tempfile from voice_mode.simple_failover import simple_stt_failover class TestSTTErrorHandling: """Test STT error handling and structured response generation""" @pytest.mark.asyncio async def test_connection_refused_all_endpoints(self): """Test when all endpoints fail with connection errors""" # Mock file object mock_file = MagicMock() with patch('voice_mode.simple_failover.AsyncOpenAI') as MockClient: # Mock connection refused for both Whisper and OpenAI mock_client = MockClient.return_value mock_client.audio.transcriptions.create = AsyncMock( side_effect=APIConnectionError( message="Connection error.", request=MagicMock() ) ) result = await simple_stt_failover(mock_file) assert result is not None assert result["error_type"] == "connection_failed" assert "attempted_endpoints" in result assert len(result["attempted_endpoints"]) == 2 # Whisper and OpenAI # Check Whisper error whisper_attempt = result["attempted_endpoints"][0] assert "127.0.0.1:2022" in whisper_attempt["endpoint"] assert whisper_attempt["provider"] == "whisper" assert "Connection error" in whisper_attempt["error"] @pytest.mark.asyncio async def test_authentication_error_openai(self): """Test when OpenAI fails with authentication error""" mock_file = MagicMock() with patch('voice_mode.simple_failover.AsyncOpenAI') as MockClient: mock_client = MockClient.return_value # First call (Whisper) - connection refused # Second call (OpenAI) - auth error mock_client.audio.transcriptions.create = AsyncMock( side_effect=[ APIConnectionError(message="Connection error.", request=MagicMock()), AuthenticationError( message="Error code: 401 - Incorrect API key provided", response=MagicMock(status_code=401), body={'error': {'message': 'Incorrect API key'}}, ) ] ) result = await simple_stt_failover(mock_file) assert result["error_type"] == "connection_failed" assert len(result["attempted_endpoints"]) == 2 # Check OpenAI error openai_attempt = result["attempted_endpoints"][1] assert openai_attempt["provider"] == "openai" assert "401" in openai_attempt["error"] or "Incorrect API key" in openai_attempt["error"] @pytest.mark.asyncio async def test_no_api_key_error(self): """Test when OPENAI_API_KEY is not set""" mock_file = MagicMock() with patch('voice_mode.simple_failover.AsyncOpenAI') as MockClient: # Simulate the OpenAI client initialization error when no API key def raise_no_api_key(*args, **kwargs): if kwargs.get('api_key') == 'dummy-key-for-local': return MockClient.return_value raise OpenAIError( "The api_key client option must be set either by passing api_key to the client or by setting the OPENAI_API_KEY environment variable" ) MockClient.side_effect = raise_no_api_key mock_client = MockClient.return_value mock_client.audio.transcriptions.create = AsyncMock( side_effect=APIConnectionError(message="Connection error.", request=MagicMock()) ) with patch('voice_mode.simple_failover.OPENAI_API_KEY', None): result = await simple_stt_failover(mock_file) assert result["error_type"] == "connection_failed" @pytest.mark.asyncio async def test_wrong_endpoint_path(self): """Test when Whisper is on wrong endpoint path""" mock_file = MagicMock() with patch('voice_mode.simple_failover.AsyncOpenAI') as MockClient: mock_client = MockClient.return_value # Simulate 404 error from wrong path mock_response = MagicMock(spec=Response) mock_response.status_code = 404 mock_response.headers = MagicMock() mock_response.headers.get = MagicMock(return_value=None) mock_request = MagicMock(spec=Request) mock_client.audio.transcriptions.create = AsyncMock( side_effect=NotFoundError( message="File Not Found (/audio/transcriptions)", response=mock_response, body="File Not Found (/audio/transcriptions)", ) ) result = await simple_stt_failover(mock_file) assert result["error_type"] == "connection_failed" whisper_attempt = result["attempted_endpoints"][0] assert "404" in whisper_attempt["error"] or "Not Found" in whisper_attempt["error"] @pytest.mark.asyncio async def test_successful_but_no_speech(self): """Test when STT connects successfully but detects no speech""" mock_file = MagicMock() with patch('voice_mode.simple_failover.AsyncOpenAI') as MockClient: mock_client = MockClient.return_value # Return empty string (no speech detected) mock_client.audio.transcriptions.create = AsyncMock( return_value="" ) result = await simple_stt_failover(mock_file) assert result["error_type"] == "no_speech" # Provider could be either whisper or openai depending on which succeeds assert result["provider"] in ["whisper", "openai"] @pytest.mark.asyncio async def test_whisper_error_as_json_text(self): """Test when Whisper returns error as JSON in text field""" mock_file = MagicMock() with patch('voice_mode.simple_failover.AsyncOpenAI') as MockClient: mock_client = MockClient.return_value # Whisper returns error as JSON string in successful response mock_client.audio.transcriptions.create = AsyncMock( return_value='{"error":"failed to read audio data"}' ) result = await simple_stt_failover(mock_file) # This is treated as successful transcription of the error message assert "text" in result assert result["text"] == '{"error":"failed to read audio data"}' assert result["provider"] == "whisper" @pytest.mark.asyncio async def test_successful_transcription(self): """Test successful transcription""" mock_file = MagicMock() with patch('voice_mode.simple_failover.AsyncOpenAI') as MockClient: mock_client = MockClient.return_value # Successful transcription mock_client.audio.transcriptions.create = AsyncMock( return_value="Hello, this is a test transcription." ) result = await simple_stt_failover(mock_file) assert "text" in result assert result["text"] == "Hello, this is a test transcription." assert result["provider"] == "whisper" assert "error_type" not in result @pytest.mark.asyncio async def test_fallback_to_openai(self): """Test fallback from Whisper to OpenAI""" mock_file = MagicMock() with patch('voice_mode.simple_failover.AsyncOpenAI') as MockClient: # Need to handle different clients for Whisper and OpenAI whisper_client = MagicMock() openai_client = MagicMock() # Track which client is being created call_count = 0 def create_client(*args, **kwargs): nonlocal call_count call_count += 1 if call_count == 1: # First call is Whisper whisper_client.audio.transcriptions.create = AsyncMock( side_effect=APIConnectionError( message="Connection error.", request=MagicMock() ) ) return whisper_client else: # Second call is OpenAI openai_client.audio.transcriptions.create = AsyncMock( return_value="Transcribed by OpenAI" ) return openai_client MockClient.side_effect = create_client result = await simple_stt_failover(mock_file) assert "text" in result assert result["text"] == "Transcribed by OpenAI" assert result["provider"] == "openai" @pytest.mark.asyncio async def test_mixed_results_prefer_successful(self): """Test that successful empty result is preferred over connection errors""" mock_file = MagicMock() with patch('voice_mode.simple_failover.AsyncOpenAI') as MockClient: whisper_client = MagicMock() openai_client = MagicMock() call_count = 0 def create_client(*args, **kwargs): nonlocal call_count call_count += 1 if call_count == 1: # Whisper fails whisper_client.audio.transcriptions.create = AsyncMock( side_effect=APIConnectionError( message="Connection error.", request=MagicMock() ) ) return whisper_client else: # OpenAI succeeds but returns empty openai_client.audio.transcriptions.create = AsyncMock( return_value="" ) return openai_client MockClient.side_effect = create_client result = await simple_stt_failover(mock_file) # Should report no_speech, not connection_failed assert result["error_type"] == "no_speech" assert result["provider"] == "openai"

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