@@ -144,6 +144,28 @@ def get_status() -> str:
144144 assert attempts ["count" ] == 3
145145
146146
147+ def test_poll_until_terminal_status_retries_server_errors ():
148+ attempts = {"count" : 0 }
149+
150+ def get_status () -> str :
151+ attempts ["count" ] += 1
152+ if attempts ["count" ] < 3 :
153+ raise HyperbrowserError ("server failure" , status_code = 503 )
154+ return "completed"
155+
156+ status = poll_until_terminal_status (
157+ operation_name = "sync poll server error retries" ,
158+ get_status = get_status ,
159+ is_terminal_status = lambda value : value == "completed" ,
160+ poll_interval_seconds = 0.0001 ,
161+ max_wait_seconds = 1.0 ,
162+ max_status_failures = 5 ,
163+ )
164+
165+ assert status == "completed"
166+ assert attempts ["count" ] == 3
167+
168+
147169def test_poll_until_terminal_status_raises_after_status_failures ():
148170 with pytest .raises (
149171 HyperbrowserPollingError , match = "Failed to poll sync poll failure"
@@ -183,6 +205,26 @@ def get_status() -> object:
183205 assert attempts ["count" ] == 1
184206
185207
208+ def test_poll_until_terminal_status_fails_fast_when_terminal_callback_raises ():
209+ attempts = {"count" : 0 }
210+
211+ def get_status () -> str :
212+ attempts ["count" ] += 1
213+ return "completed"
214+
215+ with pytest .raises (HyperbrowserError , match = "is_terminal_status failed" ):
216+ poll_until_terminal_status (
217+ operation_name = "sync terminal callback exception" ,
218+ get_status = get_status ,
219+ is_terminal_status = lambda value : (_ for _ in ()).throw (ValueError ("boom" )),
220+ poll_interval_seconds = 0.0001 ,
221+ max_wait_seconds = 1.0 ,
222+ max_status_failures = 5 ,
223+ )
224+
225+ assert attempts ["count" ] == 1
226+
227+
186228def test_retry_operation_retries_and_returns_value ():
187229 attempts = {"count" : 0 }
188230
@@ -230,6 +272,26 @@ def operation() -> str:
230272 assert attempts ["count" ] == 1
231273
232274
275+ def test_retry_operation_retries_server_errors ():
276+ attempts = {"count" : 0 }
277+
278+ def operation () -> str :
279+ attempts ["count" ] += 1
280+ if attempts ["count" ] < 3 :
281+ raise HyperbrowserError ("server failure" , status_code = 502 )
282+ return "ok"
283+
284+ result = retry_operation (
285+ operation_name = "sync retry server error" ,
286+ operation = operation ,
287+ max_attempts = 5 ,
288+ retry_delay_seconds = 0.0001 ,
289+ )
290+
291+ assert result == "ok"
292+ assert attempts ["count" ] == 3
293+
294+
233295def test_retry_operation_rejects_awaitable_operation_result ():
234296 async def async_operation () -> str :
235297 return "ok"
@@ -323,6 +385,56 @@ async def get_status() -> str:
323385 asyncio .run (run ())
324386
325387
388+ def test_poll_until_terminal_status_async_retries_server_errors ():
389+ async def run () -> None :
390+ attempts = {"count" : 0 }
391+
392+ async def get_status () -> str :
393+ attempts ["count" ] += 1
394+ if attempts ["count" ] < 3 :
395+ raise HyperbrowserError ("server failure" , status_code = 503 )
396+ return "completed"
397+
398+ status = await poll_until_terminal_status_async (
399+ operation_name = "async poll server error retries" ,
400+ get_status = get_status ,
401+ is_terminal_status = lambda value : value == "completed" ,
402+ poll_interval_seconds = 0.0001 ,
403+ max_wait_seconds = 1.0 ,
404+ max_status_failures = 5 ,
405+ )
406+
407+ assert status == "completed"
408+ assert attempts ["count" ] == 3
409+
410+ asyncio .run (run ())
411+
412+
413+ def test_poll_until_terminal_status_async_fails_fast_when_terminal_callback_raises ():
414+ async def run () -> None :
415+ attempts = {"count" : 0 }
416+
417+ async def get_status () -> str :
418+ attempts ["count" ] += 1
419+ return "completed"
420+
421+ with pytest .raises (HyperbrowserError , match = "is_terminal_status failed" ):
422+ await poll_until_terminal_status_async (
423+ operation_name = "async terminal callback exception" ,
424+ get_status = get_status ,
425+ is_terminal_status = lambda value : (_ for _ in ()).throw (
426+ ValueError ("boom" )
427+ ),
428+ poll_interval_seconds = 0.0001 ,
429+ max_wait_seconds = 1.0 ,
430+ max_status_failures = 5 ,
431+ )
432+
433+ assert attempts ["count" ] == 1
434+
435+ asyncio .run (run ())
436+
437+
326438def test_retry_operation_async_does_not_retry_non_retryable_client_errors ():
327439 async def run () -> None :
328440 attempts = {"count" : 0 }
@@ -344,6 +456,29 @@ async def operation() -> str:
344456 asyncio .run (run ())
345457
346458
459+ def test_retry_operation_async_retries_server_errors ():
460+ async def run () -> None :
461+ attempts = {"count" : 0 }
462+
463+ async def operation () -> str :
464+ attempts ["count" ] += 1
465+ if attempts ["count" ] < 3 :
466+ raise HyperbrowserError ("server failure" , status_code = 503 )
467+ return "ok"
468+
469+ result = await retry_operation_async (
470+ operation_name = "async retry server error" ,
471+ operation = operation ,
472+ max_attempts = 5 ,
473+ retry_delay_seconds = 0.0001 ,
474+ )
475+
476+ assert result == "ok"
477+ assert attempts ["count" ] == 3
478+
479+ asyncio .run (run ())
480+
481+
347482def test_async_poll_until_terminal_status_allows_immediate_terminal_on_zero_max_wait ():
348483 async def run () -> None :
349484 status = await poll_until_terminal_status_async (
@@ -871,6 +1006,31 @@ def get_next_page(page: int) -> dict:
8711006 assert attempts ["count" ] == 1
8721007
8731008
1009+ def test_collect_paginated_results_retries_server_errors ():
1010+ attempts = {"count" : 0 }
1011+ collected = []
1012+
1013+ def get_next_page (page : int ) -> dict :
1014+ attempts ["count" ] += 1
1015+ if attempts ["count" ] < 3 :
1016+ raise HyperbrowserError ("server failure" , status_code = 502 )
1017+ return {"current" : 1 , "total" : 1 , "items" : ["a" ]}
1018+
1019+ collect_paginated_results (
1020+ operation_name = "sync paginated server error retries" ,
1021+ get_next_page = get_next_page ,
1022+ get_current_page_batch = lambda response : response ["current" ],
1023+ get_total_page_batches = lambda response : response ["total" ],
1024+ on_page_success = lambda response : collected .extend (response ["items" ]),
1025+ max_wait_seconds = 1.0 ,
1026+ max_attempts = 5 ,
1027+ retry_delay_seconds = 0.0001 ,
1028+ )
1029+
1030+ assert collected == ["a" ]
1031+ assert attempts ["count" ] == 3
1032+
1033+
8741034def test_collect_paginated_results_raises_when_page_batch_stagnates ():
8751035 with pytest .raises (HyperbrowserPollingError , match = "No pagination progress" ):
8761036 collect_paginated_results (
@@ -1062,6 +1222,34 @@ async def get_next_page(page: int) -> dict:
10621222 asyncio .run (run ())
10631223
10641224
1225+ def test_collect_paginated_results_async_retries_server_errors ():
1226+ async def run () -> None :
1227+ attempts = {"count" : 0 }
1228+ collected = []
1229+
1230+ async def get_next_page (page : int ) -> dict :
1231+ attempts ["count" ] += 1
1232+ if attempts ["count" ] < 3 :
1233+ raise HyperbrowserError ("server failure" , status_code = 503 )
1234+ return {"current" : 1 , "total" : 1 , "items" : ["a" ]}
1235+
1236+ await collect_paginated_results_async (
1237+ operation_name = "async paginated server error retries" ,
1238+ get_next_page = get_next_page ,
1239+ get_current_page_batch = lambda response : response ["current" ],
1240+ get_total_page_batches = lambda response : response ["total" ],
1241+ on_page_success = lambda response : collected .extend (response ["items" ]),
1242+ max_wait_seconds = 1.0 ,
1243+ max_attempts = 5 ,
1244+ retry_delay_seconds = 0.0001 ,
1245+ )
1246+
1247+ assert collected == ["a" ]
1248+ assert attempts ["count" ] == 3
1249+
1250+ asyncio .run (run ())
1251+
1252+
10651253def test_wait_for_job_result_returns_fetched_value ():
10661254 status_values = iter (["running" , "completed" ])
10671255
0 commit comments