Skip to content

Commit 33dda71

Browse files
committed
Add unit tests for retry and token tracker modules
1 parent ffc4b74 commit 33dda71

2 files changed

Lines changed: 573 additions & 0 deletions

File tree

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
"""Tests for retry configuration and error handling."""
2+
3+
from unittest.mock import patch
4+
5+
from aieng.agent_evals.knowledge_qa.retry import (
6+
API_RETRY_INITIAL_WAIT,
7+
API_RETRY_JITTER,
8+
API_RETRY_MAX_ATTEMPTS,
9+
API_RETRY_MAX_WAIT,
10+
MAX_EMPTY_RESPONSE_RETRIES,
11+
is_context_overflow_error,
12+
is_retryable_api_error,
13+
)
14+
15+
16+
class FakeClientError(Exception):
17+
"""Fake ClientError for testing isinstance checks without API credentials."""
18+
19+
20+
class TestRetryConstants:
21+
"""Tests for retry configuration constants."""
22+
23+
def test_max_empty_response_retries(self):
24+
"""Test MAX_EMPTY_RESPONSE_RETRIES constant value."""
25+
assert MAX_EMPTY_RESPONSE_RETRIES == 2
26+
27+
def test_api_retry_max_attempts(self):
28+
"""Test API_RETRY_MAX_ATTEMPTS constant value."""
29+
assert API_RETRY_MAX_ATTEMPTS == 5
30+
31+
def test_api_retry_initial_wait(self):
32+
"""Test API_RETRY_INITIAL_WAIT constant value in seconds."""
33+
assert API_RETRY_INITIAL_WAIT == 1
34+
35+
def test_api_retry_max_wait(self):
36+
"""Test API_RETRY_MAX_WAIT constant value in seconds."""
37+
assert API_RETRY_MAX_WAIT == 60
38+
39+
def test_api_retry_jitter(self):
40+
"""Test API_RETRY_JITTER constant value in seconds."""
41+
assert API_RETRY_JITTER == 5
42+
43+
44+
class TestIsRetryableApiError:
45+
"""Tests for the is_retryable_api_error function."""
46+
47+
def test_returns_true_for_429_error(self):
48+
"""Test returns True when error message contains '429'."""
49+
with patch("aieng.agent_evals.knowledge_qa.retry.ClientError", FakeClientError):
50+
assert is_retryable_api_error(FakeClientError("Error 429: Too Many Requests")) is True
51+
52+
def test_returns_true_for_resource_exhausted(self):
53+
"""Test returns True when error message contains 'resource_exhausted'."""
54+
with patch("aieng.agent_evals.knowledge_qa.retry.ClientError", FakeClientError):
55+
assert is_retryable_api_error(FakeClientError("RESOURCE_EXHAUSTED: API limit hit")) is True
56+
57+
def test_returns_true_for_quota_error(self):
58+
"""Test returns True when error message contains 'quota'."""
59+
with patch("aieng.agent_evals.knowledge_qa.retry.ClientError", FakeClientError):
60+
assert is_retryable_api_error(FakeClientError("quota limit reached for this project")) is True
61+
62+
def test_returns_true_for_mixed_case_429(self):
63+
"""Test case-insensitive match for rate limit errors containing '429'."""
64+
with patch("aieng.agent_evals.knowledge_qa.retry.ClientError", FakeClientError):
65+
assert is_retryable_api_error(FakeClientError("Rate limit error: status=429")) is True
66+
67+
def test_returns_true_for_mixed_case_resource_exhausted(self):
68+
"""Test case-insensitive match for RESOURCE_EXHAUSTED errors."""
69+
with patch("aieng.agent_evals.knowledge_qa.retry.ClientError", FakeClientError):
70+
assert is_retryable_api_error(FakeClientError("resource_exhausted quota for gemini")) is True
71+
72+
def test_returns_true_for_mixed_case_quota(self):
73+
"""Test case-insensitive match for QUOTA errors."""
74+
with patch("aieng.agent_evals.knowledge_qa.retry.ClientError", FakeClientError):
75+
assert is_retryable_api_error(FakeClientError("QUOTA_EXCEEDED for this project")) is True
76+
77+
def test_returns_false_for_token_count_exceeds(self):
78+
"""Test returns False for context overflow 'token count exceeds'."""
79+
with patch("aieng.agent_evals.knowledge_qa.retry.ClientError", FakeClientError):
80+
assert is_retryable_api_error(FakeClientError("Token count exceeds the context window")) is False
81+
82+
def test_returns_false_for_invalid_argument_with_token(self):
83+
"""Test returns False for invalid_argument errors involving tokens."""
84+
with patch("aieng.agent_evals.knowledge_qa.retry.ClientError", FakeClientError):
85+
assert is_retryable_api_error(FakeClientError("INVALID_ARGUMENT: token limit exceeded")) is False
86+
87+
def test_returns_false_for_cache_expired(self):
88+
"""Test returns False for cache expiration errors."""
89+
with patch("aieng.agent_evals.knowledge_qa.retry.ClientError", FakeClientError):
90+
assert is_retryable_api_error(FakeClientError("Cache has expired for this request")) is False
91+
92+
def test_returns_false_for_non_client_error_with_rate_limit_text(self):
93+
"""Test returns False for non-ClientError even with rate limit keywords."""
94+
assert is_retryable_api_error(ValueError("rate limit 429")) is False
95+
96+
def test_returns_false_for_base_exception(self):
97+
"""Test returns False for plain BaseException."""
98+
assert is_retryable_api_error(Exception("quota resource_exhausted")) is False
99+
100+
def test_returns_false_for_runtime_error(self):
101+
"""Test returns False for RuntimeError with rate limit text."""
102+
assert is_retryable_api_error(RuntimeError("resource_exhausted quota exceeded")) is False
103+
104+
def test_returns_false_for_other_client_error(self):
105+
"""Test returns False for ClientError without any retryable keywords."""
106+
with patch("aieng.agent_evals.knowledge_qa.retry.ClientError", FakeClientError):
107+
assert is_retryable_api_error(FakeClientError("Bad request: unknown field")) is False
108+
109+
def test_returns_false_for_client_error_with_token_no_rate_limit(self):
110+
"""Test returns False for ClientError with 'token' but no rate limit."""
111+
with patch("aieng.agent_evals.knowledge_qa.retry.ClientError", FakeClientError):
112+
assert is_retryable_api_error(FakeClientError("token refresh failed")) is False
113+
114+
def test_context_overflow_takes_precedence_over_rate_limit(self):
115+
"""Test context overflow early-exit occurs before rate limit check."""
116+
with patch("aieng.agent_evals.knowledge_qa.retry.ClientError", FakeClientError):
117+
# Message matches both context overflow and rate limit patterns
118+
error = FakeClientError("token count exceeds limit, status 429 quota")
119+
assert is_retryable_api_error(error) is False
120+
121+
def test_cache_expired_takes_precedence_over_rate_limit(self):
122+
"""Test cache expiration early-exit occurs before rate limit check."""
123+
with patch("aieng.agent_evals.knowledge_qa.retry.ClientError", FakeClientError):
124+
# Message matches both cache expiration and rate limit patterns
125+
error = FakeClientError("cache expired and quota resource_exhausted")
126+
assert is_retryable_api_error(error) is False
127+
128+
def test_invalid_argument_without_token_does_not_block_rate_limit(self):
129+
"""Test invalid_argument without 'token' does not suppress rate limit retry."""
130+
with patch("aieng.agent_evals.knowledge_qa.retry.ClientError", FakeClientError):
131+
# no "token" → not context overflow → falls through to rate limit
132+
error = FakeClientError("INVALID_ARGUMENT: bad request quota 429")
133+
assert is_retryable_api_error(error) is True
134+
135+
136+
class TestIsContextOverflowError:
137+
"""Tests for the is_context_overflow_error function."""
138+
139+
def test_returns_true_for_token_count_exceeds(self):
140+
"""Test returns True when error message contains 'token count exceeds'."""
141+
with patch("aieng.agent_evals.knowledge_qa.retry.ClientError", FakeClientError):
142+
assert is_context_overflow_error(FakeClientError("Token count exceeds the context window")) is True
143+
144+
def test_returns_true_for_invalid_argument_with_token(self):
145+
"""Test returns True for invalid_argument errors with 'token' in message."""
146+
with patch("aieng.agent_evals.knowledge_qa.retry.ClientError", FakeClientError):
147+
assert is_context_overflow_error(FakeClientError("INVALID_ARGUMENT: token limit exceeded")) is True
148+
149+
def test_returns_true_for_mixed_case_token_count_exceeds(self):
150+
"""Test case-insensitive match for 'TOKEN COUNT EXCEEDS'."""
151+
with patch("aieng.agent_evals.knowledge_qa.retry.ClientError", FakeClientError):
152+
assert is_context_overflow_error(FakeClientError("TOKEN COUNT EXCEEDS maximum limit")) is True
153+
154+
def test_returns_true_for_mixed_case_invalid_argument_token(self):
155+
"""Test case-insensitive match for INVALID_ARGUMENT + TOKEN."""
156+
with patch("aieng.agent_evals.knowledge_qa.retry.ClientError", FakeClientError):
157+
assert is_context_overflow_error(FakeClientError("INVALID_ARGUMENT: TOKEN limit exceeded")) is True
158+
159+
def test_returns_false_for_rate_limit_429(self):
160+
"""Test returns False for 429 rate limit errors."""
161+
with patch("aieng.agent_evals.knowledge_qa.retry.ClientError", FakeClientError):
162+
assert is_context_overflow_error(FakeClientError("429 Too Many Requests")) is False
163+
164+
def test_returns_false_for_resource_exhausted(self):
165+
"""Test returns False for resource exhausted errors."""
166+
with patch("aieng.agent_evals.knowledge_qa.retry.ClientError", FakeClientError):
167+
assert is_context_overflow_error(FakeClientError("RESOURCE_EXHAUSTED: quota exceeded")) is False
168+
169+
def test_returns_false_for_cache_expired(self):
170+
"""Test returns False for cache expiration errors."""
171+
with patch("aieng.agent_evals.knowledge_qa.retry.ClientError", FakeClientError):
172+
assert is_context_overflow_error(FakeClientError("cache has expired")) is False
173+
174+
def test_returns_false_for_invalid_argument_without_token(self):
175+
"""Test returns False when 'invalid_argument' present but 'token' is absent."""
176+
with patch("aieng.agent_evals.knowledge_qa.retry.ClientError", FakeClientError):
177+
assert is_context_overflow_error(FakeClientError("INVALID_ARGUMENT: bad field value")) is False
178+
179+
def test_returns_false_for_token_without_invalid_argument_or_token_count_exceeds(self):
180+
"""Test returns False when 'token' appears alone without matching patterns."""
181+
with patch("aieng.agent_evals.knowledge_qa.retry.ClientError", FakeClientError):
182+
assert is_context_overflow_error(FakeClientError("token refresh failed")) is False
183+
184+
def test_returns_false_for_non_client_error_with_overflow_text(self):
185+
"""Test returns False for non-ClientError even with overflow keywords."""
186+
assert is_context_overflow_error(ValueError("token count exceeds limit")) is False
187+
188+
def test_returns_false_for_base_exception(self):
189+
"""Test returns False for plain Exception with context overflow text."""
190+
assert is_context_overflow_error(Exception("token count exceeds")) is False
191+
192+
def test_returns_false_for_other_client_error(self):
193+
"""Test returns False for ClientError without context overflow indicators."""
194+
with patch("aieng.agent_evals.knowledge_qa.retry.ClientError", FakeClientError):
195+
assert is_context_overflow_error(FakeClientError("Internal server error occurred")) is False

0 commit comments

Comments
 (0)