11import json
2+
23from unittest .mock import AsyncMock , MagicMock , patch
34
45import httpx
@@ -232,14 +233,49 @@ async def mock_send_stream_request(*args, **kwargs):
232233 assert events [1 ] == StreamResponse (message = Message (message_id = 'msg-123' ))
233234
234235
236+ def create_405_error ():
237+ mock_response = MagicMock (spec = httpx .Response )
238+ mock_response .status_code = 405
239+ mock_response .json .return_value = {
240+ 'type' : 'MethodNotAllowed' ,
241+ 'message' : 'Method Not Allowed' ,
242+ }
243+ mock_request = MagicMock (spec = httpx .Request )
244+ mock_request .url = 'http://example.com/v1/tasks/task-123:subscribe'
245+
246+ status_error = httpx .HTTPStatusError (
247+ '405 Method Not Allowed' , request = mock_request , response = mock_response
248+ )
249+ raise A2AClientError ('HTTP Error 405' ) from status_error
250+
251+
252+ def create_500_error ():
253+ mock_response = MagicMock (spec = httpx .Response )
254+ mock_response .status_code = 500
255+ mock_response .json .return_value = {
256+ 'type' : 'InternalError' ,
257+ 'message' : 'Internal Error' ,
258+ }
259+ mock_request = MagicMock (spec = httpx .Request )
260+
261+ status_error = httpx .HTTPStatusError (
262+ '500 Internal Error' , request = mock_request , response = mock_response
263+ )
264+ raise A2AClientError ('HTTP Error 500' ) from status_error
265+
266+
235267@pytest .mark .asyncio
236- async def test_compat_rest_transport_subscribe (transport ):
237- async def mock_send_stream_request (* args , ** kwargs ):
268+ async def test_compat_rest_transport_subscribe_post_works_no_retry (transport ):
269+ """Scenario: POST works, no retry."""
270+
271+ async def mock_stream (method , path , context = None , json = None ):
272+ assert method == 'POST'
273+ assert json == {'id' : 'task-123' }
238274 task = Task (id = 'task-123' )
239275 task .status .message .role = Role .ROLE_AGENT
240276 yield StreamResponse (task = task )
241277
242- transport ._send_stream_request = mock_send_stream_request
278+ transport ._send_stream_request = mock_stream
243279
244280 req = SubscribeToTaskRequest (id = 'task-123' )
245281 events = [event async for event in transport .subscribe (req )]
@@ -248,6 +284,109 @@ async def mock_send_stream_request(*args, **kwargs):
248284 expected_task = Task (id = 'task-123' )
249285 expected_task .status .message .role = Role .ROLE_AGENT
250286 assert events [0 ] == StreamResponse (task = expected_task )
287+ assert transport ._subscribe_method == 'POST'
288+ assert transport ._subscribe_retry_attempted is False
289+
290+
291+ @pytest .mark .asyncio
292+ async def test_compat_rest_transport_subscribe_post_405_retry_get_success (
293+ transport ,
294+ ):
295+ """Scenario: POST returns 405, automatic retry GET. Second call uses GET directly."""
296+ call_count = 0
297+
298+ async def mock_stream (method , path , context = None , json = None ):
299+ nonlocal call_count
300+ call_count += 1
301+ if method == 'POST' :
302+ assert json == {'id' : 'task-123' }
303+ create_405_error ()
304+ if method == 'GET' :
305+ assert json is None
306+ task = Task (id = 'task-123' )
307+ task .status .message .role = Role .ROLE_AGENT
308+ yield StreamResponse (task = task )
309+
310+ transport ._send_stream_request = mock_stream
311+
312+ req = SubscribeToTaskRequest (id = 'task-123' )
313+ events = [event async for event in transport .subscribe (req )]
314+
315+ assert len (events ) == 1
316+ assert call_count == 2
317+ assert transport ._subscribe_method == 'GET'
318+ assert transport ._subscribe_retry_attempted is True
319+
320+ # Second call should use GET directly
321+ call_count = 0
322+ events = [event async for event in transport .subscribe (req )]
323+ assert len (events ) == 1
324+ assert call_count == 1 # Only GET called
325+ assert transport ._subscribe_method == 'GET'
326+
327+
328+ @pytest .mark .asyncio
329+ async def test_compat_rest_transport_subscribe_post_405_get_405_fails (
330+ transport ,
331+ ):
332+ """Scenario: POST return 405, retry GET, return 405 - error. Second call is just POST."""
333+ call_count = 0
334+
335+ async def mock_stream (method , path , context = None , json = None ):
336+ nonlocal call_count
337+ call_count += 1
338+ if method == 'POST' :
339+ assert json == {'id' : 'task-123' }
340+ elif method == 'GET' :
341+ assert json is None
342+ # To make it an async generator even when it raises
343+ if False :
344+ yield
345+ create_405_error ()
346+
347+ transport ._send_stream_request = mock_stream
348+
349+ req = SubscribeToTaskRequest (id = 'task-123' )
350+ with pytest .raises (A2AClientError ) as exc_info :
351+ [event async for event in transport .subscribe (req )]
352+
353+ assert '405' in str (exc_info .value )
354+ assert call_count == 2 # Tried POST then GET
355+ assert transport ._subscribe_method == 'POST'
356+ assert transport ._subscribe_retry_attempted is True
357+
358+ # Second call should try POST directly and fail without retry
359+ call_count = 0
360+ with pytest .raises (A2AClientError ):
361+ [event async for event in transport .subscribe (req )]
362+ assert call_count == 1
363+ assert transport ._subscribe_method == 'POST'
364+
365+
366+ @pytest .mark .asyncio
367+ async def test_compat_rest_transport_subscribe_post_500_no_retry (transport ):
368+ """Scenario: POST return 500, no automatic retry."""
369+ call_count = 0
370+
371+ async def mock_stream (method , path , context = None , json = None ):
372+ nonlocal call_count
373+ call_count += 1
374+ assert method == 'POST'
375+ assert json == {'id' : 'task-123' }
376+ if False :
377+ yield
378+ create_500_error ()
379+
380+ transport ._send_stream_request = mock_stream
381+
382+ req = SubscribeToTaskRequest (id = 'task-123' )
383+ with pytest .raises (A2AClientError ) as exc_info :
384+ [event async for event in transport .subscribe (req )]
385+
386+ assert '500' in str (exc_info .value )
387+ assert call_count == 1 # No retry on 500
388+ assert transport ._subscribe_method == 'POST'
389+ assert transport ._subscribe_retry_attempted is False
251390
252391
253392def test_compat_rest_transport_handle_http_error (transport ):
0 commit comments