From ddf13f70e9ba8b3536757c5af7ebda0b019c4351 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 18 Sep 2026 12:06:36 +0000 Subject: [PATCH] feature: add unit tests for generic_call_api_with_retries retry/backoff paths Co-Authored-By: Gilad Samuels --- tests/llm/test_llm_utils.py | 126 ++++++++++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) diff --git a/tests/llm/test_llm_utils.py b/tests/llm/test_llm_utils.py index d7c42853d..ab3078a51 100644 --- a/tests/llm/test_llm_utils.py +++ b/tests/llm/test_llm_utils.py @@ -216,6 +216,132 @@ def test_retry_parse_raises(): llm_utils.retry(mock_chat, llm_utils.Discussion(), 3, parser_raises) +class FakeRateLimitError(Exception): + pass + + +class FakeAPIError(Exception): + def __init__(self, message="api error", status_code=None): + super().__init__(message) + self.status_code = status_code + + +def fake_get_status_code(exception): + return getattr(exception, "status_code", None) + + +@pytest.fixture +def recorded_sleeps(monkeypatch): + sleeps = [] + monkeypatch.setattr(llm_utils.time, "sleep", lambda delay: sleeps.append(delay)) + return sleeps + + +def call_with_retries(client_function, **kwargs): + params = dict( + client_function=client_function, + api_params={}, + is_response_valid_fn=lambda response: response == "valid", + rate_limit_exceptions=(FakeRateLimitError,), + api_error_exceptions=(FakeAPIError,), + get_status_code_fn=fake_get_status_code, + max_retries=4, + initial_retry_delay_seconds=2, + max_retry_delay_seconds=10, + ) + params.update(kwargs) + return llm_utils.generic_call_api_with_retries(**params) + + +def test_generic_call_api_succeeds_without_sleeping(recorded_sleeps): + client = Mock(return_value="valid") + + assert call_with_retries(client) == "valid" + assert client.call_count == 1 + assert recorded_sleeps == [] + + +def test_generic_call_api_retries_rate_limit_error(recorded_sleeps): + client = Mock(side_effect=[FakeRateLimitError("slow down"), "valid"]) + + assert call_with_retries(client) == "valid" + assert client.call_count == 2 + assert recorded_sleeps == [4] # initial_delay * 2 ** attempt, attempt == 1 + + +@pytest.mark.parametrize("status_code", [429, 500, 503]) +def test_generic_call_api_retries_retriable_status_codes(recorded_sleeps, status_code): + client = Mock(side_effect=[FakeAPIError(status_code=status_code), "valid"]) + + assert call_with_retries(client) == "valid" + assert client.call_count == 2 + assert recorded_sleeps == [4] + + +@pytest.mark.parametrize("status_code", [400, 401, 404, None]) +def test_generic_call_api_reraises_non_retriable_status_codes(recorded_sleeps, status_code): + client = Mock(side_effect=FakeAPIError(status_code=status_code)) + + with pytest.raises(FakeAPIError): + call_with_retries(client) + + assert client.call_count == 1 + assert recorded_sleeps == [] + + +def test_generic_call_api_reraises_unexpected_exception(recorded_sleeps): + client = Mock(side_effect=ValueError("boom")) + + with pytest.raises(ValueError): + call_with_retries(client) + + assert client.call_count == 1 + assert recorded_sleeps == [] + + +def test_generic_call_api_retries_invalid_response(recorded_sleeps): + client = Mock(side_effect=["invalid", "invalid", "valid"]) + + assert call_with_retries(client) == "valid" + assert client.call_count == 3 + # invalid content uses attempt - 1 as the exponent + assert recorded_sleeps == [2, 4] + + +def test_generic_call_api_raises_runtime_error_when_retries_exhausted(recorded_sleeps): + client = Mock(return_value="invalid") + + with pytest.raises(RuntimeError, match="failed after 4 retries"): + call_with_retries(client) + + assert client.call_count == 4 + # no sleep on the last attempt, which gives up instead of retrying + assert recorded_sleeps == [2, 4, 8] + + +def test_generic_call_api_raises_runtime_error_after_repeated_rate_limits(recorded_sleeps): + client = Mock(side_effect=FakeRateLimitError("slow down")) + + with pytest.raises(RuntimeError): + call_with_retries(client) + + assert client.call_count == 4 + assert recorded_sleeps == [4, 8, 10] # capped by max_retry_delay_seconds + + +def test_generic_call_api_reraises_when_status_code_fn_fails(recorded_sleeps): + client = Mock(side_effect=FakeAPIError(status_code=429)) + + def failing_status_fn(exception): + raise AttributeError("no status code") + + with pytest.raises(FakeAPIError): + call_with_retries(client, get_status_code_fn=failing_status_fn) + + assert client.call_count == 1 + assert recorded_sleeps == [] + + def test_extract_code_blocks(): text = """\ This is some text.