""" Extended tests for rate_limiting/llm/wrapper.py — active rate limiting paths. The existing test_llm_rate_limiting.py only covers disabled rate limiting. Two tests are skipped with "Rate limiting is disabled by default". These tests exercise the actual rate-limiting code paths. Tests cover: - AdaptiveLLMWait.__call__: tracker wait, retry-after merging, error recording - RateLimitedLLMWrapper._check_if_local_model: provider + URL detection - RateLimitedLLMWrapper._do_invoke: error wrapping - RateLimitedLLMWrapper.invoke with rate limiting active - __str__ and __repr__ """ from unittest.mock import Mock import pytest WRAPPER_MODULE = ( "local_deep_research.web_search_engines.rate_limiting.llm.wrapper" ) # --------------------------------------------------------------------------- # AdaptiveLLMWait.__call__ # --------------------------------------------------------------------------- class TestAdaptiveLLMWait: """Tests for AdaptiveLLMWait wait strategy.""" def test_returns_tracker_wait_time_no_error(self): """Returns tracker wait time when no error.""" from local_deep_research.web_search_engines.rate_limiting.llm.wrapper import ( AdaptiveLLMWait, ) mock_tracker = Mock() mock_tracker.get_wait_time.return_value = 5.0 wait = AdaptiveLLMWait(mock_tracker, "openai-api") # Simulate retry_state with no failure retry_state = Mock() retry_state.outcome = None result = wait(retry_state) assert result == 5.0 mock_tracker.get_wait_time.assert_called_once_with("openai-api") def test_returns_max_of_tracker_and_retry_after(self): """Returns max(tracker_wait, retry_after) when error has retry-after.""" from local_deep_research.web_search_engines.rate_limiting.llm.wrapper import ( AdaptiveLLMWait, ) mock_tracker = Mock() mock_tracker.get_wait_time.return_value = 2.0 wait = AdaptiveLLMWait(mock_tracker, "openai-api") # First call: record an error with retry-after error = Mock() error.response = Mock() error.response.headers = {"Retry-After": "10"} error.__str__ = lambda self: "rate limit" retry_state = Mock() retry_state.outcome = Mock() retry_state.outcome.failed = True retry_state.outcome.exception.return_value = error result = wait(retry_state) # Should be max(2.0, 10.0) = 10.0 assert result == 10.0 def test_records_last_error_from_failed_outcome(self): """Records last_error from failed outcome.""" from local_deep_research.web_search_engines.rate_limiting.llm.wrapper import ( AdaptiveLLMWait, ) mock_tracker = Mock() mock_tracker.get_wait_time.return_value = 1.0 wait = AdaptiveLLMWait(mock_tracker, "test") error = Exception("rate limit hit") retry_state = Mock() retry_state.outcome = Mock() retry_state.outcome.failed = True retry_state.outcome.exception.return_value = error wait(retry_state) assert wait.last_error is error def test_uses_last_error_across_calls(self): """last_error persists and is used in subsequent calls.""" from local_deep_research.web_search_engines.rate_limiting.llm.wrapper import ( AdaptiveLLMWait, ) mock_tracker = Mock() mock_tracker.get_wait_time.return_value = 1.0 wait = AdaptiveLLMWait(mock_tracker, "test") # First call: record error with retry-after in message error = Exception("retry after 30 seconds") retry_state = Mock() retry_state.outcome = Mock() retry_state.outcome.failed = True retry_state.outcome.exception.return_value = error result1 = wait(retry_state) assert result1 == 30.0 # Second call: no new error, but last_error still has retry-after retry_state2 = Mock() retry_state2.outcome = None result2 = wait(retry_state2) assert result2 == 30.0 # --------------------------------------------------------------------------- # RateLimitedLLMWrapper._check_if_local_model # --------------------------------------------------------------------------- class TestCheckIfLocalModel: """Tests for _check_if_local_model.""" def test_true_for_local_providers(self): """Returns True for known local provider names.""" from local_deep_research.web_search_engines.rate_limiting.llm.wrapper import ( create_rate_limited_llm_wrapper, ) for provider in [ "ollama", "lmstudio", "llamacpp", "local", "none", ]: base_llm = Mock() wrapper = create_rate_limited_llm_wrapper( base_llm, provider=provider ) assert wrapper._check_if_local_model() is True, ( f"Failed for {provider}" ) def test_true_for_localhost_base_url(self): """Returns True when base_url contains localhost.""" from local_deep_research.web_search_engines.rate_limiting.llm.wrapper import ( create_rate_limited_llm_wrapper, ) base_llm = Mock() base_llm.base_url = "http://localhost:11434/v1" wrapper = create_rate_limited_llm_wrapper(base_llm, provider="custom") assert wrapper._check_if_local_model() is True def test_true_for_127_0_0_1_base_url(self): """Returns True when base_url contains 127.0.0.1.""" from local_deep_research.web_search_engines.rate_limiting.llm.wrapper import ( create_rate_limited_llm_wrapper, ) base_llm = Mock() base_llm.base_url = "http://127.0.0.1:8080/api" wrapper = create_rate_limited_llm_wrapper(base_llm, provider="custom") assert wrapper._check_if_local_model() is True def test_false_for_remote_urls(self): """Returns False for remote URLs and providers.""" from local_deep_research.web_search_engines.rate_limiting.llm.wrapper import ( create_rate_limited_llm_wrapper, ) base_llm = Mock() base_llm.base_url = "https://api.openai.com/v1" wrapper = create_rate_limited_llm_wrapper(base_llm, provider="openai") assert wrapper._check_if_local_model() is False # --------------------------------------------------------------------------- # RateLimitedLLMWrapper._do_invoke # --------------------------------------------------------------------------- class TestDoInvoke: """Tests for _do_invoke.""" def test_rate_limit_error_wrapped_into_rate_limit_error(self): """Rate limit errors are wrapped into RateLimitError.""" from local_deep_research.web_search_engines.rate_limiting.llm.wrapper import ( create_rate_limited_llm_wrapper, ) from local_deep_research.web_search_engines.rate_limiting.exceptions import ( RateLimitError, ) base_llm = Mock() base_llm.invoke.side_effect = Exception( "429 Too Many Requests rate limit" ) wrapper = create_rate_limited_llm_wrapper(base_llm) with pytest.raises(RateLimitError, match="LLM rate limit"): wrapper._do_invoke("test prompt") def test_non_rate_limit_error_passed_through(self): """Non-rate-limit errors pass through unchanged.""" from local_deep_research.web_search_engines.rate_limiting.llm.wrapper import ( create_rate_limited_llm_wrapper, ) base_llm = Mock() base_llm.invoke.side_effect = ValueError("Invalid input format") wrapper = create_rate_limited_llm_wrapper(base_llm) with pytest.raises(ValueError, match="Invalid input format"): wrapper._do_invoke("test prompt") def test_successful_invoke_returns_result(self): """Successful invoke returns result.""" from local_deep_research.web_search_engines.rate_limiting.llm.wrapper import ( create_rate_limited_llm_wrapper, ) base_llm = Mock() base_llm.invoke.return_value = "LLM response" wrapper = create_rate_limited_llm_wrapper(base_llm) result = wrapper._do_invoke("test prompt") assert result == "LLM response" # --------------------------------------------------------------------------- # RateLimitedLLMWrapper.invoke with rate limiting active # --------------------------------------------------------------------------- class TestInvokeWithRateLimiting: """Tests for invoke() with rate limiting enabled.""" def test_successful_first_attempt_records_success(self): """Successful first attempt records success in tracker.""" from local_deep_research.web_search_engines.rate_limiting.llm.wrapper import ( create_rate_limited_llm_wrapper, ) base_llm = Mock() base_llm.invoke.return_value = "response" mock_tracker = Mock() mock_tracker.get_wait_time.return_value = 0 wrapper = create_rate_limited_llm_wrapper(base_llm, provider="openai") # Force-enable rate limiting wrapper.rate_limiter = mock_tracker result = wrapper.invoke("prompt") assert result == "response" mock_tracker.record_outcome.assert_called_once_with( engine_type=wrapper._get_rate_limit_key(), wait_time=0, success=True, retry_count=0, ) def test_invoke_without_rate_limiter_delegates_directly(self): """Without rate limiter, invoke delegates to _do_invoke.""" from local_deep_research.web_search_engines.rate_limiting.llm.wrapper import ( create_rate_limited_llm_wrapper, ) base_llm = Mock() base_llm.invoke.return_value = "direct response" wrapper = create_rate_limited_llm_wrapper(base_llm) assert wrapper.rate_limiter is None result = wrapper.invoke("prompt") assert result == "direct response" base_llm.invoke.assert_called_once_with("prompt") def test_rate_limit_error_records_failure(self): """Rate limit error is recorded as failure in tracker.""" from local_deep_research.web_search_engines.rate_limiting.llm.wrapper import ( create_rate_limited_llm_wrapper, ) base_llm = Mock() base_llm.invoke.side_effect = Exception("429 rate limit exceeded") mock_tracker = Mock() mock_tracker.get_wait_time.return_value = 0 wrapper = create_rate_limited_llm_wrapper(base_llm, provider="openai") wrapper.rate_limiter = mock_tracker # tenacity wraps the final error in RetryError after exhausting retries with pytest.raises(Exception): wrapper.invoke("prompt") # Should have recorded a failure failure_calls = [ c for c in mock_tracker.record_outcome.call_args_list if c.kwargs.get("success") is False ] assert len(failure_calls) >= 1 # --------------------------------------------------------------------------- # __str__ and __repr__ # --------------------------------------------------------------------------- class TestStringRepresentations: """Tests for __str__ and __repr__.""" def test_str_format(self): """__str__ returns expected format.""" from local_deep_research.web_search_engines.rate_limiting.llm.wrapper import ( create_rate_limited_llm_wrapper, ) base_llm = Mock() base_llm.__str__ = lambda self: "MockLLM" wrapper = create_rate_limited_llm_wrapper(base_llm) assert str(wrapper) == "RateLimited(MockLLM)" def test_repr_format(self): """__repr__ returns expected format.""" from local_deep_research.web_search_engines.rate_limiting.llm.wrapper import ( create_rate_limited_llm_wrapper, ) base_llm = Mock() wrapper = create_rate_limited_llm_wrapper(base_llm) result = repr(wrapper) assert result.startswith("RateLimitedLLMWrapper(")