@@ -325,75 +325,62 @@ def test_use_stale_cache_on_fetch_failure_with_warning(
325325 warning_call = mock_log .warning .call_args
326326 self .assertIn ("using stale cache" , warning_call [0 ][0 ])
327327
328+ @parameterized .expand (
329+ [
330+ # The Retry-After cooldown (300s) outlives the 60s default, so the
331+ # holds after second 1460 prove the server's value governs.
332+ ("server_error" , 500 , None , 60.0 ),
333+ ("rate_limited_retry_after" , 429 , {"Retry-After" : "300" }, 300.0 ),
334+ ]
335+ )
328336 @patch ("posthog.ai.prompts._get_session" )
329337 @patch ("posthog.ai.prompts.time.time" )
330- def test_hold_a_cooldown_after_a_failed_refetch_then_retry (
331- self , mock_time , mock_get_session
338+ def test_hold_a_cooldown_after_each_failed_refetch (
339+ self , _scenario , status , headers , cooldown , mock_time , mock_get_session
332340 ):
333341 # Without the cooldown, one throttled client turns every later get()
334342 # into another network request until one succeeds.
335343 mock_get = mock_get_session .return_value .get
336344 mock_get .side_effect = [
337345 MockResponse (json_data = self .mock_prompt_response ),
338- MockResponse (status_code = 500 , ok = False ),
346+ MockResponse (status_code = status , ok = False , headers = headers ),
347+ MockResponse (status_code = status , ok = False , headers = headers ),
339348 MockResponse (json_data = self .mock_prompt_response ),
340349 ]
341350 mock_time .return_value = 1000.0
342351
343352 prompts = Prompts (self .create_mock_posthog ())
344353 prompts .get ("test-prompt" , cache_ttl_seconds = 300 , with_metadata = False )
345354
346- # Past TTL: the refetch fails, stale cache is served, cooldown starts.
355+ # Past TTL: the refetch fails, stale cache is served, a cooldown starts.
347356 mock_time .return_value = 1400.0
348357 result = prompts .get ("test-prompt" , cache_ttl_seconds = 300 , with_metadata = True )
349358 self .assertEqual (result .source , "stale_cache" )
350359 self .assertEqual (mock_get .call_count , 2 )
351360
352- # Within the 60s cooldown: stale cache again, no network attempt.
353- mock_time .return_value = 1430.0
361+ # Within the cooldown: stale cache again, no network attempt.
362+ mock_time .return_value = 1400.0 + cooldown - 1
354363 result = prompts .get ("test-prompt" , cache_ttl_seconds = 300 , with_metadata = True )
355364 self .assertEqual (result .source , "stale_cache" )
356365 self .assertEqual (mock_get .call_count , 2 )
357366
358- # Past the cooldown: the network is retried and the cache refreshed.
359- mock_time .return_value = 1470.0
360- result = prompts .get ("test-prompt" , cache_ttl_seconds = 300 , with_metadata = True )
361- self .assertEqual (result .source , "api" )
362- self .assertEqual (mock_get .call_count , 3 )
363-
364- @patch ("posthog.ai.prompts._get_session" )
365- @patch ("posthog.ai.prompts.time.time" )
366- def test_hold_the_cooldown_for_the_retry_after_a_429_sends (
367- self , mock_time , mock_get_session
368- ):
369- # A rate-limited client must wait as long as the server told it to,
370- # not the 60s default.
371- mock_get = mock_get_session .return_value .get
372- mock_get .side_effect = [
373- MockResponse (json_data = self .mock_prompt_response ),
374- MockResponse (status_code = 429 , ok = False , headers = {"Retry-After" : "300" }),
375- MockResponse (json_data = self .mock_prompt_response ),
376- ]
377- mock_time .return_value = 1000.0
378-
379- prompts = Prompts (self .create_mock_posthog ())
380- prompts .get ("test-prompt" , cache_ttl_seconds = 300 , with_metadata = False )
381-
382- mock_time .return_value = 1400.0
367+ # Past the cooldown: the retry fails too and a new cooldown starts.
368+ mock_time .return_value = 1400.0 + cooldown + 1
383369 result = prompts .get ("test-prompt" , cache_ttl_seconds = 300 , with_metadata = True )
384370 self .assertEqual (result .source , "stale_cache" )
371+ self .assertEqual (mock_get .call_count , 3 )
385372
386- # Past the 60s default but within Retry-After : no network attempt.
387- mock_time .return_value = 1600.0
373+ # Within the second cooldown : no network attempt.
374+ mock_time .return_value = 1400.0 + 2 * cooldown
388375 result = prompts .get ("test-prompt" , cache_ttl_seconds = 300 , with_metadata = True )
389376 self .assertEqual (result .source , "stale_cache" )
390- self .assertEqual (mock_get .call_count , 2 )
377+ self .assertEqual (mock_get .call_count , 3 )
391378
392- # Past Retry-After : the network is retried.
393- mock_time .return_value = 1701.0
379+ # Past the second cooldown : the network is retried and the cache refreshed .
380+ mock_time .return_value = 1400.0 + 2 * cooldown + 2
394381 result = prompts .get ("test-prompt" , cache_ttl_seconds = 300 , with_metadata = True )
395382 self .assertEqual (result .source , "api" )
396- self .assertEqual (mock_get .call_count , 3 )
383+ self .assertEqual (mock_get .call_count , 4 )
397384
398385 @patch ("posthog.ai.prompts._get_session" )
399386 @patch ("posthog.ai.prompts.log" )
@@ -619,6 +606,18 @@ def test_default_cache_ttl_seconds_zero_disables_caching(
619606 prompts .get ("test-prompt" , with_metadata = False )
620607 self .assertEqual (mock_get .call_count , 2 )
621608
609+ # A failed refetch must not start a cooldown here: a zero TTL is an
610+ # explicit request to refetch on every read.
611+ mock_get .side_effect = [
612+ MockResponse (status_code = 500 , ok = False ),
613+ MockResponse (json_data = self .mock_prompt_response ),
614+ ]
615+ result = prompts .get ("test-prompt" , with_metadata = True )
616+ self .assertEqual (result .source , "stale_cache" )
617+ result = prompts .get ("test-prompt" , with_metadata = True )
618+ self .assertEqual (result .source , "api" )
619+ self .assertEqual (mock_get .call_count , 4 )
620+
622621 @patch ("posthog.ai.prompts._get_session" )
623622 def test_url_encode_prompt_names_with_special_characters (self , mock_get_session ):
624623 """Should URL-encode prompt names with special characters."""
0 commit comments