Files
wehub-resource-sync 7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

356 lines
12 KiB
Python

"""
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(")