""" Extended tests for rate_limiting/llm/detection.py — provider-specific detection. The existing test_llm_rate_limiting.py covers basic patterns but misses: - OpenAI-specific module check (line 52) - Anthropic-specific module check (line 58) - extract_retry_after edge cases (date headers, decimals, multiple patterns) Tests cover: - OpenAI provider-specific detection (module == "openai") - Anthropic provider-specific detection ("anthropic" in module) - extract_retry_after edge cases """ from unittest.mock import Mock def _make_exception(class_name, module, message): """Create an exception instance with a custom __class__.__module__ and __name__. Python builtins don't support __class__ reassignment, so we create a new exception type with the desired module and instantiate directly. """ cls = type(class_name, (Exception,), {"__module__": module}) return cls(message) # --------------------------------------------------------------------------- # OpenAI-specific detection # --------------------------------------------------------------------------- class TestOpenAISpecificDetection: """Tests for OpenAI provider-specific rate limit detection.""" def test_openai_module_ratelimiterror_429_detected(self): """OpenAI module + ratelimiterror type + '429' -> True.""" from local_deep_research.web_search_engines.rate_limiting.llm.detection import ( is_llm_rate_limit_error, ) error = _make_exception("RateLimitError", "openai", "Error code: 429") assert is_llm_rate_limit_error(error) is True def test_openai_module_non_429_not_detected(self): """OpenAI module but non-rate-limit error -> not detected.""" from local_deep_research.web_search_engines.rate_limiting.llm.detection import ( is_llm_rate_limit_error, ) error = _make_exception( "AuthenticationError", "openai", "Invalid API key" ) assert is_llm_rate_limit_error(error) is False def test_openai_apierror_with_429_detected(self): """OpenAI ApiError with '429' in message -> True.""" from local_deep_research.web_search_engines.rate_limiting.llm.detection import ( is_llm_rate_limit_error, ) error = _make_exception( "ApiError", "openai", "Request failed with status 429" ) assert is_llm_rate_limit_error(error) is True # --------------------------------------------------------------------------- # Anthropic-specific detection # --------------------------------------------------------------------------- class TestAnthropicSpecificDetection: """Tests for Anthropic provider-specific rate limit detection.""" def test_anthropic_module_rate_limit_detected(self): """'anthropic' in module + 'rate_limit' in message -> True.""" from local_deep_research.web_search_engines.rate_limiting.llm.detection import ( is_llm_rate_limit_error, ) error = _make_exception( "RateLimitError", "anthropic._exceptions", "rate_limit_error: too many requests", ) assert is_llm_rate_limit_error(error) is True def test_anthropic_module_429_detected(self): """'anthropic' in module + '429' in message -> True.""" from local_deep_research.web_search_engines.rate_limiting.llm.detection import ( is_llm_rate_limit_error, ) error = _make_exception( "ApiStatusError", "anthropic._exceptions", "HTTP 429 from Anthropic API", ) assert is_llm_rate_limit_error(error) is True def test_anthropic_module_unrelated_error_not_detected(self): """'anthropic' in module + unrelated message -> False.""" from local_deep_research.web_search_engines.rate_limiting.llm.detection import ( is_llm_rate_limit_error, ) error = _make_exception( "AuthenticationError", "anthropic._exceptions", "Invalid API key provided", ) assert is_llm_rate_limit_error(error) is False # --------------------------------------------------------------------------- # extract_retry_after edge cases # --------------------------------------------------------------------------- class TestExtractRetryAfterEdgeCases: """Edge cases for extract_retry_after.""" def test_non_numeric_retry_after_header(self): """Non-numeric Retry-After header (date string) is handled gracefully.""" from local_deep_research.web_search_engines.rate_limiting.llm.detection import ( extract_retry_after, ) error = Mock() error.response = Mock() error.response.headers = { "Retry-After": "Wed, 21 Oct 2015 07:28:00 GMT" } error.__str__ = lambda self: "Rate limited" # Date string can't be parsed as float, falls through to message parsing result = extract_retry_after(error) assert isinstance(result, (int, float)) def test_decimal_seconds_in_message(self): """'retry after 30.5 seconds' with decimal -> 30.5.""" from local_deep_research.web_search_engines.rate_limiting.llm.detection import ( extract_retry_after, ) error = Exception("Please retry after 30.5 seconds") result = extract_retry_after(error) assert result == 30.5 def test_multiple_patterns_returns_first_match(self): """Multiple patterns in same message -> first match returned.""" from local_deep_research.web_search_engines.rate_limiting.llm.detection import ( extract_retry_after, ) # "try again in X seconds" pattern comes before "wait X seconds" error = Exception( "Rate limited. Try again in 15 seconds. Or wait 30 seconds." ) result = extract_retry_after(error) assert result == 15.0 def test_no_patterns_returns_zero(self): """No matching patterns -> returns 0.""" from local_deep_research.web_search_engines.rate_limiting.llm.detection import ( extract_retry_after, ) error = Exception("Something went wrong with no timing info") result = extract_retry_after(error) assert result == 0