@@ -102,6 +102,48 @@ def get_status() -> str:
102102 assert status == "completed"
103103
104104
105+ def test_poll_until_terminal_status_does_not_retry_non_retryable_client_errors ():
106+ attempts = {"count" : 0 }
107+
108+ def get_status () -> str :
109+ attempts ["count" ] += 1
110+ raise HyperbrowserError ("client failure" , status_code = 400 )
111+
112+ with pytest .raises (HyperbrowserError , match = "client failure" ):
113+ poll_until_terminal_status (
114+ operation_name = "sync poll client error" ,
115+ get_status = get_status ,
116+ is_terminal_status = lambda value : value == "completed" ,
117+ poll_interval_seconds = 0.0001 ,
118+ max_wait_seconds = 1.0 ,
119+ max_status_failures = 5 ,
120+ )
121+
122+ assert attempts ["count" ] == 1
123+
124+
125+ def test_poll_until_terminal_status_retries_rate_limit_errors ():
126+ attempts = {"count" : 0 }
127+
128+ def get_status () -> str :
129+ attempts ["count" ] += 1
130+ if attempts ["count" ] < 3 :
131+ raise HyperbrowserError ("rate limited" , status_code = 429 )
132+ return "completed"
133+
134+ status = poll_until_terminal_status (
135+ operation_name = "sync poll rate limit retries" ,
136+ get_status = get_status ,
137+ is_terminal_status = lambda value : value == "completed" ,
138+ poll_interval_seconds = 0.0001 ,
139+ max_wait_seconds = 1.0 ,
140+ max_status_failures = 5 ,
141+ )
142+
143+ assert status == "completed"
144+ assert attempts ["count" ] == 3
145+
146+
105147def test_poll_until_terminal_status_raises_after_status_failures ():
106148 with pytest .raises (
107149 HyperbrowserPollingError , match = "Failed to poll sync poll failure"
@@ -170,6 +212,24 @@ def test_retry_operation_raises_after_max_attempts():
170212 )
171213
172214
215+ def test_retry_operation_does_not_retry_non_retryable_client_errors ():
216+ attempts = {"count" : 0 }
217+
218+ def operation () -> str :
219+ attempts ["count" ] += 1
220+ raise HyperbrowserError ("client failure" , status_code = 404 )
221+
222+ with pytest .raises (HyperbrowserError , match = "client failure" ):
223+ retry_operation (
224+ operation_name = "sync retry client error" ,
225+ operation = operation ,
226+ max_attempts = 5 ,
227+ retry_delay_seconds = 0.0001 ,
228+ )
229+
230+ assert attempts ["count" ] == 1
231+
232+
173233def test_retry_operation_rejects_awaitable_operation_result ():
174234 async def async_operation () -> str :
175235 return "ok"
@@ -240,6 +300,50 @@ async def run() -> None:
240300 asyncio .run (run ())
241301
242302
303+ def test_poll_until_terminal_status_async_does_not_retry_non_retryable_client_errors ():
304+ async def run () -> None :
305+ attempts = {"count" : 0 }
306+
307+ async def get_status () -> str :
308+ attempts ["count" ] += 1
309+ raise HyperbrowserError ("client failure" , status_code = 400 )
310+
311+ with pytest .raises (HyperbrowserError , match = "client failure" ):
312+ await poll_until_terminal_status_async (
313+ operation_name = "async poll client error" ,
314+ get_status = get_status ,
315+ is_terminal_status = lambda value : value == "completed" ,
316+ poll_interval_seconds = 0.0001 ,
317+ max_wait_seconds = 1.0 ,
318+ max_status_failures = 5 ,
319+ )
320+
321+ assert attempts ["count" ] == 1
322+
323+ asyncio .run (run ())
324+
325+
326+ def test_retry_operation_async_does_not_retry_non_retryable_client_errors ():
327+ async def run () -> None :
328+ attempts = {"count" : 0 }
329+
330+ async def operation () -> str :
331+ attempts ["count" ] += 1
332+ raise HyperbrowserError ("client failure" , status_code = 400 )
333+
334+ with pytest .raises (HyperbrowserError , match = "client failure" ):
335+ await retry_operation_async (
336+ operation_name = "async retry client error" ,
337+ operation = operation ,
338+ max_attempts = 5 ,
339+ retry_delay_seconds = 0.0001 ,
340+ )
341+
342+ assert attempts ["count" ] == 1
343+
344+ asyncio .run (run ())
345+
346+
243347def test_async_poll_until_terminal_status_allows_immediate_terminal_on_zero_max_wait ():
244348 async def run () -> None :
245349 status = await poll_until_terminal_status_async (
@@ -745,6 +849,28 @@ def test_collect_paginated_results_raises_after_page_failures():
745849 )
746850
747851
852+ def test_collect_paginated_results_does_not_retry_non_retryable_client_errors ():
853+ attempts = {"count" : 0 }
854+
855+ def get_next_page (page : int ) -> dict :
856+ attempts ["count" ] += 1
857+ raise HyperbrowserError ("client failure" , status_code = 400 )
858+
859+ with pytest .raises (HyperbrowserError , match = "client failure" ):
860+ collect_paginated_results (
861+ operation_name = "sync paginated client error" ,
862+ get_next_page = get_next_page ,
863+ get_current_page_batch = lambda response : response ["current" ],
864+ get_total_page_batches = lambda response : response ["total" ],
865+ on_page_success = lambda response : None ,
866+ max_wait_seconds = 1.0 ,
867+ max_attempts = 5 ,
868+ retry_delay_seconds = 0.0001 ,
869+ )
870+
871+ assert attempts ["count" ] == 1
872+
873+
748874def test_collect_paginated_results_raises_when_page_batch_stagnates ():
749875 with pytest .raises (HyperbrowserPollingError , match = "No pagination progress" ):
750876 collect_paginated_results (
@@ -911,6 +1037,31 @@ async def run() -> None:
9111037 asyncio .run (run ())
9121038
9131039
1040+ def test_collect_paginated_results_async_does_not_retry_non_retryable_client_errors ():
1041+ async def run () -> None :
1042+ attempts = {"count" : 0 }
1043+
1044+ async def get_next_page (page : int ) -> dict :
1045+ attempts ["count" ] += 1
1046+ raise HyperbrowserError ("client failure" , status_code = 404 )
1047+
1048+ with pytest .raises (HyperbrowserError , match = "client failure" ):
1049+ await collect_paginated_results_async (
1050+ operation_name = "async paginated client error" ,
1051+ get_next_page = get_next_page ,
1052+ get_current_page_batch = lambda response : response ["current" ],
1053+ get_total_page_batches = lambda response : response ["total" ],
1054+ on_page_success = lambda response : None ,
1055+ max_wait_seconds = 1.0 ,
1056+ max_attempts = 5 ,
1057+ retry_delay_seconds = 0.0001 ,
1058+ )
1059+
1060+ assert attempts ["count" ] == 1
1061+
1062+ asyncio .run (run ())
1063+
1064+
9141065def test_wait_for_job_result_returns_fetched_value ():
9151066 status_values = iter (["running" , "completed" ])
9161067
0 commit comments