11import asyncio
22import json
33from unittest .mock import AsyncMock , MagicMock , patch
4+
45import pytest
56
67from elevenlabs .conversational_ai .conversation import (
7- AsyncConversation ,
88 AsyncAudioInterface ,
9+ AsyncConversation ,
910 ConversationInitiationData ,
1011)
1112
@@ -45,7 +46,6 @@ def create_mock_async_websocket(messages=None):
4546
4647 # Convert messages to JSON strings
4748 json_messages = [json .dumps (msg ) for msg in messages ]
48- json_messages .extend (['{"type": "keep_alive"}' ] * 10 ) # Add some keep-alive messages
4949
5050 # Create an iterator
5151 message_iter = iter (json_messages )
@@ -54,8 +54,9 @@ async def mock_recv():
5454 try :
5555 return next (message_iter )
5656 except StopIteration :
57- # Simulate connection close after messages
58- raise asyncio .TimeoutError ()
57+ # After all messages, simulate timeout by sleeping forever
58+ # This will be caught by asyncio.wait_for timeout in the conversation
59+ await asyncio .sleep (float ("inf" ))
5960
6061 mock_ws .recv = mock_recv
6162 return mock_ws
@@ -66,6 +67,7 @@ async def test_async_conversation_basic_flow():
6667 # Mock setup
6768 mock_ws = create_mock_async_websocket ()
6869 mock_client = MagicMock ()
70+ mock_client ._client_wrapper .get_base_url .return_value = "https://api.elevenlabs.io"
6971 agent_response_callback = AsyncMock ()
7072 test_user_id = "test_user_123"
7173
@@ -94,7 +96,7 @@ async def test_async_conversation_basic_flow():
9496
9597 # Assertions - check the call was made with the right structure
9698 send_calls = [call [0 ][0 ] for call in mock_ws .send .call_args_list ]
97- init_messages = [json .loads (call ) for call in send_calls if ' conversation_initiation_client_data' in call ]
99+ init_messages = [json .loads (call ) for call in send_calls if " conversation_initiation_client_data" in call ]
98100 assert len (init_messages ) == 1
99101 init_message = init_messages [0 ]
100102
@@ -148,6 +150,7 @@ async def test_async_conversation_with_dynamic_variables():
148150 # Mock setup
149151 mock_ws = create_mock_async_websocket ()
150152 mock_client = MagicMock ()
153+ mock_client ._client_wrapper .get_base_url .return_value = "https://api.elevenlabs.io"
151154 agent_response_callback = AsyncMock ()
152155
153156 dynamic_variables = {"name" : "angelo" }
@@ -177,7 +180,7 @@ async def test_async_conversation_with_dynamic_variables():
177180
178181 # Assertions - check the call was made with the right structure
179182 send_calls = [call [0 ][0 ] for call in mock_ws .send .call_args_list ]
180- init_messages = [json .loads (call ) for call in send_calls if ' conversation_initiation_client_data' in call ]
183+ init_messages = [json .loads (call ) for call in send_calls if " conversation_initiation_client_data" in call ]
181184 assert len (init_messages ) == 1
182185 init_message = init_messages [0 ]
183186
@@ -196,6 +199,7 @@ async def test_async_conversation_with_contextual_update():
196199 # Mock setup
197200 mock_ws = create_mock_async_websocket ([])
198201 mock_client = MagicMock ()
202+ mock_client ._client_wrapper .get_base_url .return_value = "https://api.elevenlabs.io"
199203
200204 # Setup the conversation
201205 conversation = AsyncConversation (
@@ -228,6 +232,7 @@ async def test_async_conversation_send_user_message():
228232 # Mock setup
229233 mock_ws = create_mock_async_websocket ([])
230234 mock_client = MagicMock ()
235+ mock_client ._client_wrapper .get_base_url .return_value = "https://api.elevenlabs.io"
231236
232237 # Setup the conversation
233238 conversation = AsyncConversation (
@@ -260,6 +265,7 @@ async def test_async_conversation_register_user_activity():
260265 # Mock setup
261266 mock_ws = create_mock_async_websocket ([])
262267 mock_client = MagicMock ()
268+ mock_client ._client_wrapper .get_base_url .return_value = "https://api.elevenlabs.io"
263269
264270 # Setup the conversation
265271 conversation = AsyncConversation (
@@ -300,29 +306,21 @@ async def test_async_conversation_callback_flows():
300306 "type" : "agent_response_correction" ,
301307 "agent_response_correction_event" : {
302308 "original_agent_response" : "Hello ther!" ,
303- "corrected_agent_response" : "Hello there!"
304- }
305- },
306- {
307- "type" : "user_transcript" ,
308- "user_transcription_event" : {"user_transcript" : "Hi, how are you?" }
309- },
310- {
311- "type" : "ping" ,
312- "ping_event" : {"event_id" : "123" , "ping_ms" : 50 }
313- },
314- {
315- "type" : "interruption" ,
316- "interruption_event" : {"event_id" : "456" }
309+ "corrected_agent_response" : "Hello there!" ,
310+ },
317311 },
312+ {"type" : "user_transcript" , "user_transcription_event" : {"user_transcript" : "Hi, how are you?" }},
313+ {"type" : "ping" , "ping_event" : {"event_id" : "123" , "ping_ms" : 50 }},
314+ {"type" : "interruption" , "interruption_event" : {"event_id" : "456" }},
318315 {
319316 "type" : "audio" ,
320- "audio_event" : {"event_id" : "789" , "audio_base_64" : "dGVzdA==" } # "test" in base64
321- }
317+ "audio_event" : {"event_id" : "789" , "audio_base_64" : "dGVzdA==" }, # "test" in base64
318+ },
322319 ]
323320
324321 mock_ws = create_mock_async_websocket (messages )
325322 mock_client = MagicMock ()
323+ mock_client ._client_wrapper .get_base_url .return_value = "https://api.elevenlabs.io"
326324
327325 # Setup callbacks
328326 agent_response_callback = AsyncMock ()
@@ -368,7 +366,6 @@ async def test_async_conversation_callback_flows():
368366
369367@pytest .mark .asyncio
370368async def test_async_conversation_wss_url_generation_without_get_environment ():
371-
372369 from elevenlabs .core .client_wrapper import SyncClientWrapper
373370
374371 # Test with various base URL formats to ensure robustness
@@ -383,24 +380,20 @@ async def test_async_conversation_wss_url_generation_without_get_environment():
383380 # Create a real SyncClientWrapper to ensure it doesn't have get_environment method
384381 mock_client = MagicMock ()
385382 mock_client ._client_wrapper = SyncClientWrapper (
386- base_url = base_url ,
387- api_key = "test_key" ,
388- httpx_client = MagicMock (),
389- timeout = 30.0
383+ base_url = base_url , api_key = "test_key" , httpx_client = MagicMock (), timeout = 30.0
390384 )
391385
392386 conversation = AsyncConversation (
393- client = mock_client ,
394- agent_id = TEST_AGENT_ID ,
395- requires_auth = False ,
396- audio_interface = MockAsyncAudioInterface ()
387+ client = mock_client , agent_id = TEST_AGENT_ID , requires_auth = False , audio_interface = MockAsyncAudioInterface ()
397388 )
398389
399390 try :
400391 wss_url = conversation ._get_wss_url ()
401392
402393 # Verify the URL is correctly generated
403- expected_url = f"{ expected_ws_base } /v1/convai/conversation?agent_id={ TEST_AGENT_ID } &source=python_sdk&version="
394+ expected_url = (
395+ f"{ expected_ws_base } /v1/convai/conversation?agent_id={ TEST_AGENT_ID } &source=python_sdk&version="
396+ )
404397 assert wss_url .startswith (expected_url ), f"URL should start with { expected_url } , got { wss_url } "
405398
406399 # Verify the URL contains version parameter
@@ -414,3 +407,44 @@ async def test_async_conversation_wss_url_generation_without_get_environment():
414407
415408 except Exception as e :
416409 assert False , f"Unexpected error generating WebSocket URL: { e } "
410+
411+
412+ @pytest .mark .asyncio
413+ async def test_async_websocket_url_construction_edge_cases ():
414+ """Test WebSocket URL construction edge cases for async conversation, specifically for trailing slash handling."""
415+ from elevenlabs .conversational_ai .conversation import AsyncConversation
416+ from elevenlabs .core .client_wrapper import SyncClientWrapper
417+
418+ # Test cases with various base URL formats
419+ test_cases = [
420+ # Base URLs without trailing slashes (the main edge case)
421+ ("https://api.eu.residency.elevenlabs.io" , "wss://api.eu.residency.elevenlabs.io" ),
422+ ("https://api.elevenlabs.io" , "wss://api.elevenlabs.io" ),
423+ ("http://localhost:8000" , "ws://localhost:8000" ),
424+ # Base URLs with trailing slashes (should still work)
425+ ("https://api.eu.residency.elevenlabs.io/" , "wss://api.eu.residency.elevenlabs.io" ),
426+ ("https://api.elevenlabs.io/" , "wss://api.elevenlabs.io" ),
427+ ("http://localhost:8000/" , "ws://localhost:8000" ),
428+ ]
429+
430+ for base_url , expected_ws_base in test_cases :
431+ # Test async conversation WebSocket URL construction
432+ mock_client = MagicMock ()
433+ mock_client ._client_wrapper = SyncClientWrapper (
434+ base_url = base_url , api_key = "test_key" , httpx_client = MagicMock (), timeout = 30.0
435+ )
436+
437+ conversation = AsyncConversation (
438+ client = mock_client , agent_id = TEST_AGENT_ID , requires_auth = False , audio_interface = MockAsyncAudioInterface ()
439+ )
440+
441+ # Test conversation URL generation
442+ conv_url = conversation ._get_wss_url ()
443+ expected_conv_url = f"{ expected_ws_base } /v1/convai/conversation"
444+ assert (
445+ expected_conv_url in conv_url
446+ ), f"Async conversation URL should contain { expected_conv_url } , got { conv_url } "
447+
448+ # Ensure no double slashes in the path (except after the protocol)
449+ url_path = conv_url .split ("://" , 1 )[1 ] # Remove protocol
450+ assert "//" not in url_path , f"Async conversation URL should not contain double slashes in path: { conv_url } "
0 commit comments