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

767 lines
26 KiB
Python

"""
Tests for BaseSearchEngine.
"""
import json
from pathlib import Path
from unittest.mock import Mock
from local_deep_research.config.constants import DEFAULT_MAX_FILTERED_RESULTS
from local_deep_research.web_search_engines.search_engine_base import (
BaseSearchEngine,
AdaptiveWait,
)
from local_deep_research.web_search_engines.rate_limiting import (
RateLimitError,
)
def test_default_max_filtered_results_matches_default_settings_json():
"""``DEFAULT_MAX_FILTERED_RESULTS`` (the Python fallback used when no
DB is available) must match the shipping value in
``defaults/default_settings.json`` — otherwise a user with a seeded
DB and a programmatic caller without one see different filter caps."""
defaults_path = (
Path(__file__).resolve().parents[2]
/ "src"
/ "local_deep_research"
/ "defaults"
/ "default_settings.json"
)
settings = json.loads(defaults_path.read_text())
assert (
settings["search.max_filtered_results"]["value"]
== DEFAULT_MAX_FILTERED_RESULTS
)
def _make_structured_llm(indices):
"""Create a mock LLM whose ``invoke()`` returns the given indices as
a comma-separated text response (matches the relevance_filter contract)."""
mock_llm = Mock()
mock_llm.invoke.return_value = ", ".join(str(i) for i in indices)
return mock_llm
def _make_error_llm(exc):
"""Create a mock LLM where ``invoke()`` raises."""
mock_llm = Mock()
mock_llm.invoke.side_effect = exc
return mock_llm
class TestBaseSearchEngineClassAttributes:
"""Tests for BaseSearchEngine class attributes."""
def test_is_public_default_false(self):
"""is_public defaults to False for safety."""
assert BaseSearchEngine.is_public is False
def test_is_generic_default_false(self):
"""is_generic defaults to False."""
assert BaseSearchEngine.is_generic is False
def test_is_scientific_default_false(self):
"""is_scientific defaults to False."""
assert BaseSearchEngine.is_scientific is False
def test_is_local_default_false(self):
"""is_local defaults to False."""
assert BaseSearchEngine.is_local is False
def test_is_news_default_false(self):
"""is_news defaults to False."""
assert BaseSearchEngine.is_news is False
def test_is_code_default_false(self):
"""is_code defaults to False."""
assert BaseSearchEngine.is_code is False
class TestLoadEngineClass:
"""Tests for _load_engine_class method."""
def test_load_engine_class_missing_module_path(self):
"""Returns error when module_path is missing."""
config = {"class_name": "TestEngine"}
success, engine_class, error = BaseSearchEngine._load_engine_class(
"test", config
)
assert success is False
assert engine_class is None
assert "module_path" in error
def test_load_engine_class_missing_class_name(self):
"""Returns error when class_name is missing."""
config = {"module_path": ".engines.test"}
success, engine_class, error = BaseSearchEngine._load_engine_class(
"test", config
)
assert success is False
assert engine_class is None
assert "class_name" in error
def test_load_engine_class_import_error(self):
"""Returns error when import fails."""
config = {
"module_path": ".engines.nonexistent_module",
"class_name": "NonexistentEngine",
}
success, engine_class, error = BaseSearchEngine._load_engine_class(
"test", config
)
assert success is False
assert engine_class is None
assert "Could not load" in error or "Security error" in error
def test_load_engine_class_success(self):
"""Successfully loads engine class."""
config = {
"module_path": ".engines.search_engine_wikipedia",
"class_name": "WikipediaSearchEngine",
}
success, engine_class, error = BaseSearchEngine._load_engine_class(
"wikipedia", config
)
assert success is True
assert engine_class is not None
assert error is None
class TestBaseSearchEngineSubclassing:
"""Tests for subclassing BaseSearchEngine."""
def test_subclass_can_override_attributes(self):
"""Subclass can override class attributes."""
class PublicSearchEngine(BaseSearchEngine):
is_public = True
is_generic = True
def run(self, query):
return []
assert PublicSearchEngine.is_public is True
assert PublicSearchEngine.is_generic is True
def test_subclass_scientific_engine(self):
"""Subclass can be marked as scientific."""
class ScientificEngine(BaseSearchEngine):
is_scientific = True
is_public = True
def run(self, query):
return []
assert ScientificEngine.is_scientific is True
assert ScientificEngine.is_public is True
def test_subclass_local_engine(self):
"""Subclass can be marked as local."""
class LocalEngine(BaseSearchEngine):
is_local = True
is_public = False
def run(self, query):
return []
assert LocalEngine.is_local is True
assert LocalEngine.is_public is False
def test_subclass_news_engine(self):
"""Subclass can be marked as news engine."""
class NewsEngine(BaseSearchEngine):
is_news = True
is_public = True
def run(self, query):
return []
assert NewsEngine.is_news is True
def test_subclass_code_engine(self):
"""Subclass can be marked as code engine."""
class CodeEngine(BaseSearchEngine):
is_code = True
is_public = True
def run(self, query):
return []
assert CodeEngine.is_code is True
class TestSearchResultValidation:
"""Tests for search result validation."""
def test_result_structure(self, mock_search_results):
"""Search results have expected structure."""
for result in mock_search_results:
assert "title" in result
assert "link" in result
assert "snippet" in result
assert "source" in result
def test_result_link_is_url(self, mock_search_results):
"""Result links are valid URLs."""
for result in mock_search_results:
link = result["link"]
assert link.startswith("http://") or link.startswith("https://")
def test_result_title_not_empty(self, mock_search_results):
"""Result titles are not empty."""
for result in mock_search_results:
assert len(result["title"]) > 0
class ConcreteSearchEngine(BaseSearchEngine):
"""Concrete implementation for testing."""
is_public = True
is_generic = True
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._mock_previews = []
self._mock_full_content = []
def _get_previews(self, query):
return self._mock_previews
def _get_full_content(self, relevant_items):
return self._mock_full_content or relevant_items
class TestAdaptiveWait:
"""Tests for AdaptiveWait class."""
def test_adaptive_wait_calls_function(self):
"""AdaptiveWait calls the provided function."""
mock_func = Mock(return_value=2.5)
wait = AdaptiveWait(mock_func)
mock_retry_state = Mock()
result = wait(mock_retry_state)
assert result == 2.5
mock_func.assert_called_once()
def test_adaptive_wait_with_different_values(self):
"""AdaptiveWait returns different values based on function."""
call_count = [0]
def variable_wait():
call_count[0] += 1
return call_count[0] * 1.0
wait = AdaptiveWait(variable_wait)
mock_retry_state = Mock()
assert wait(mock_retry_state) == 1.0
assert wait(mock_retry_state) == 2.0
assert wait(mock_retry_state) == 3.0
class TestBaseSearchEngineInit:
"""Tests for BaseSearchEngine initialization."""
def test_init_default_values(self):
"""Test initialization with default values."""
engine = ConcreteSearchEngine(programmatic_mode=True)
assert engine.max_results == 10
assert engine.max_filtered_results == DEFAULT_MAX_FILTERED_RESULTS
assert engine.llm is None
assert engine.search_snippets_only is True
assert engine.programmatic_mode is True
assert engine._preview_filters == []
assert engine._content_filters == []
def test_init_custom_values(self):
"""Test initialization with custom values."""
mock_llm = Mock()
engine = ConcreteSearchEngine(
llm=mock_llm,
max_results=20,
max_filtered_results=10,
search_snippets_only=False,
programmatic_mode=True,
)
assert engine.max_results == 20
assert engine.max_filtered_results == 10
assert engine.llm is mock_llm
assert engine.search_snippets_only is False
def test_init_none_values_use_defaults(self):
"""Test that None values use defaults."""
engine = ConcreteSearchEngine(
max_results=None, max_filtered_results=None, programmatic_mode=True
)
assert engine.max_results == 10
assert engine.max_filtered_results == DEFAULT_MAX_FILTERED_RESULTS
def test_init_with_filters(self):
"""Test initialization with custom filters."""
preview_filter = Mock()
content_filter = Mock()
engine = ConcreteSearchEngine(
preview_filters=[preview_filter],
content_filters=[content_filter],
programmatic_mode=True,
)
assert len(engine._preview_filters) == 1
assert len(engine._content_filters) == 1
def test_init_with_settings_snapshot(self):
"""Test initialization with settings snapshot."""
snapshot = {"key": "value"}
engine = ConcreteSearchEngine(
settings_snapshot=snapshot, programmatic_mode=True
)
assert engine.settings_snapshot == snapshot
def test_init_engine_type_set(self):
"""Test that engine_type is set from class name."""
engine = ConcreteSearchEngine(programmatic_mode=True)
assert engine.engine_type == "ConcreteSearchEngine"
class TestMaxResultsProperty:
"""Tests for max_results property."""
def test_max_results_getter(self):
"""Test max_results getter."""
engine = ConcreteSearchEngine(max_results=15, programmatic_mode=True)
assert engine.max_results == 15
def test_max_results_setter(self):
"""Test max_results setter."""
engine = ConcreteSearchEngine(programmatic_mode=True)
engine.max_results = 25
assert engine.max_results == 25
def test_max_results_setter_none_uses_default(self):
"""Test max_results setter with None uses default."""
engine = ConcreteSearchEngine(programmatic_mode=True)
engine.max_results = None
assert engine.max_results == 10
def test_max_results_minimum_is_one(self):
"""Test max_results has minimum of 1."""
engine = ConcreteSearchEngine(max_results=0, programmatic_mode=True)
assert engine.max_results >= 1
engine.max_results = -5
assert engine.max_results >= 1
class TestMaxFilteredResultsProperty:
"""Tests for max_filtered_results property."""
def test_max_filtered_results_getter(self):
"""Test max_filtered_results getter."""
engine = ConcreteSearchEngine(
max_filtered_results=8, programmatic_mode=True
)
assert engine.max_filtered_results == 8
def test_max_filtered_results_setter(self):
"""Test max_filtered_results setter."""
engine = ConcreteSearchEngine(programmatic_mode=True)
engine.max_filtered_results = 12
assert engine.max_filtered_results == 12
def test_max_filtered_results_setter_none_uses_default(self):
"""Test max_filtered_results setter with None uses default."""
engine = ConcreteSearchEngine(programmatic_mode=True)
engine.max_filtered_results = None
assert engine.max_filtered_results == DEFAULT_MAX_FILTERED_RESULTS
class TestRunMethod:
"""Tests for BaseSearchEngine.run method."""
def test_run_returns_empty_list_on_no_previews(self):
"""Test run returns empty list when no previews found."""
engine = ConcreteSearchEngine(programmatic_mode=True)
engine._mock_previews = []
results = engine.run("test query")
assert results == []
def test_run_returns_previews_with_snippets_only(self):
"""Test run returns previews when search_snippets_only is True."""
engine = ConcreteSearchEngine(
search_snippets_only=True, programmatic_mode=True
)
engine._mock_previews = [
{
"title": "Test",
"snippet": "Content",
"link": "https://example.com",
}
]
results = engine.run("test query")
assert len(results) == 1
assert results[0]["title"] == "Test"
def test_run_applies_preview_filters(self):
"""Test run applies preview filters."""
mock_filter = Mock()
mock_filter.filter_results.return_value = [
{"title": "Filtered", "snippet": "Content"}
]
engine = ConcreteSearchEngine(
preview_filters=[mock_filter], programmatic_mode=True
)
engine._mock_previews = [
{"title": "Original", "snippet": "Content"},
{"title": "Also Original", "snippet": "More content"},
]
results = engine.run("test query")
mock_filter.filter_results.assert_called_once()
assert len(results) == 1
assert results[0]["title"] == "Filtered"
def test_run_applies_content_filters(self):
"""Test run applies content filters."""
mock_filter = Mock()
mock_filter.filter_results.return_value = [
{"title": "Content Filtered", "content": "Full content"}
]
engine = ConcreteSearchEngine(
content_filters=[mock_filter],
search_snippets_only=False,
programmatic_mode=True,
)
engine._mock_previews = [{"title": "Original", "snippet": "Content"}]
engine._mock_full_content = [
{"title": "Original", "content": "Full content"}
]
results = engine.run("test query")
mock_filter.filter_results.assert_called_once()
assert len(results) == 1
assert results[0]["title"] == "Content Filtered"
def test_run_gets_full_content_when_not_snippets_only(self):
"""Test run gets full content when search_snippets_only is False."""
engine = ConcreteSearchEngine(
search_snippets_only=False, programmatic_mode=True
)
engine._mock_previews = [{"title": "Preview", "snippet": "Short"}]
engine._mock_full_content = [
{"title": "Preview", "content": "Full content"}
]
results = engine.run("test query")
assert len(results) == 1
assert results[0]["content"] == "Full content"
def test_run_handles_exception_gracefully(self):
"""Test run handles exceptions gracefully."""
class FailingEngine(ConcreteSearchEngine):
def _get_previews(self, query):
raise ValueError("Test error")
engine = FailingEngine(programmatic_mode=True)
results = engine.run("test query")
assert results == []
def test_run_with_research_context(self):
"""Test run with research context parameter."""
engine = ConcreteSearchEngine(programmatic_mode=True)
engine._mock_previews = [{"title": "Test", "snippet": "Content"}]
context = {"research_id": 123}
results = engine.run("test query", research_context=context)
assert len(results) == 1
class TestInvokeMethod:
"""Tests for invoke method (LangChain compatibility)."""
def test_invoke_calls_run(self):
"""Test invoke calls run method."""
engine = ConcreteSearchEngine(programmatic_mode=True)
engine._mock_previews = [{"title": "Test", "snippet": "Content"}]
results = engine.invoke("test query")
assert len(results) == 1
assert results[0]["title"] == "Test"
class TestFilterForRelevance:
"""Tests for _filter_for_relevance method."""
def test_filter_no_llm_returns_all(self):
"""Test that no LLM returns all previews."""
engine = ConcreteSearchEngine(llm=None, programmatic_mode=True)
previews = [
{"title": "A", "snippet": "Content A"},
{"title": "B", "snippet": "Content B"},
]
result = engine._filter_for_relevance(previews, "query")
assert len(result) == 2
def test_filter_single_preview_returns_all(self):
"""Test that single preview returns as-is."""
mock_llm = Mock()
engine = ConcreteSearchEngine(llm=mock_llm, programmatic_mode=True)
previews = [{"title": "A", "snippet": "Content A"}]
result = engine._filter_for_relevance(previews, "query")
assert len(result) == 1
mock_llm.invoke.assert_not_called()
def test_filter_with_valid_llm_response(self):
"""Test filtering with valid LLM response."""
mock_llm = _make_structured_llm([0, 2])
engine = ConcreteSearchEngine(llm=mock_llm, programmatic_mode=True)
previews = [
{"title": "A", "snippet": "Content A", "url": "https://a.com"},
{"title": "B", "snippet": "Content B", "url": "https://b.com"},
{"title": "C", "snippet": "Content C", "url": "https://c.com"},
]
result = engine._filter_for_relevance(previews, "query")
assert len(result) == 2
assert result[0]["title"] == "A"
assert result[1]["title"] == "C"
def test_filter_with_out_of_range_indices(self):
"""Test filtering skips out-of-range indices."""
mock_llm = _make_structured_llm([0, 10, 1]) # 10 is out of range
engine = ConcreteSearchEngine(llm=mock_llm, programmatic_mode=True)
previews = [
{"title": "A", "snippet": "Content A", "url": "https://a.com"},
{"title": "B", "snippet": "Content B", "url": "https://b.com"},
]
result = engine._filter_for_relevance(previews, "query")
assert len(result) == 2 # Only valid indices 0 and 1
def test_filter_with_parse_error_returns_capped_fallback(self):
"""Parse errors return a capped slice of the previews so the
downstream pipeline isn't fed empty results when the filter is
unavailable. Cap is
``max_filtered_results or DEFAULT_MAX_FILTERED_RESULTS``."""
mock_llm = _make_error_llm(
ValueError("Failed to parse structured output")
)
engine = ConcreteSearchEngine(llm=mock_llm, programmatic_mode=True)
previews = [
{"title": "A", "snippet": "Content A", "url": "https://a.com"},
{"title": "B", "snippet": "Content B", "url": "https://b.com"},
{"title": "C", "snippet": "Content C", "url": "https://c.com"},
]
result = engine._filter_for_relevance(previews, "query")
# All 3 previews fit under DEFAULT_MAX_FILTERED_RESULTS
assert result == previews
def test_filter_limits_to_max_filtered_results(self):
"""Test filtering limits results to max_filtered_results."""
mock_llm = _make_structured_llm([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
engine = ConcreteSearchEngine(
llm=mock_llm, max_filtered_results=3, programmatic_mode=True
)
previews = [
{
"title": str(i),
"snippet": f"Content {i}",
"url": f"https://{i}.com",
}
for i in range(10)
]
result = engine._filter_for_relevance(previews, "query")
assert len(result) == 3
def test_filter_handles_llm_exception(self):
"""LLM exceptions return a capped slice of the previews so the
downstream pipeline isn't fed empty results when the filter is
unavailable. Cap is
``max_filtered_results or DEFAULT_MAX_FILTERED_RESULTS``."""
mock_llm = _make_error_llm(Exception("LLM error"))
engine = ConcreteSearchEngine(llm=mock_llm, programmatic_mode=True)
previews = [
{"title": "A", "snippet": "Content A", "url": "https://a.com"},
{"title": "B", "snippet": "Content B", "url": "https://b.com"},
]
result = engine._filter_for_relevance(previews, "query")
# Both previews fit under DEFAULT_MAX_FILTERED_RESULTS
assert result == previews
def test_filter_truncates_long_snippets(self):
"""Filter must not fail on snippets longer than the internal
snippet cap (``_SNIPPET_CHAR_CAP`` in ``relevance_filter.py``).
The truncation happens inside the prompt builder, not in the
returned preview, so we just assert the call succeeds and the
cap doesn't bleed into the output."""
mock_llm = _make_structured_llm([0])
engine = ConcreteSearchEngine(llm=mock_llm, programmatic_mode=True)
long_snippet = "x" * 2000 # Longer than the 800-char cap
previews = [
{"title": "A", "snippet": long_snippet, "url": "https://a.com"},
{"title": "B", "snippet": "Short", "url": "https://b.com"},
]
result = engine._filter_for_relevance(previews, "query")
# Single index returned → one kept. Snippet in the returned
# preview is NOT truncated — the cap only applies inside the
# filter's prompt text.
assert len(result) == 1
assert result[0]["snippet"] == long_snippet
class TestRateLimiting:
"""Tests for rate limiting functionality."""
def test_get_adaptive_wait(self):
"""Test _get_adaptive_wait returns tracker wait time."""
engine = ConcreteSearchEngine(programmatic_mode=True)
engine.rate_tracker = Mock()
engine.rate_tracker.get_wait_time.return_value = 1.5
wait_time = engine._get_adaptive_wait()
assert wait_time == 1.5
assert engine._last_wait_time == 1.5
engine.rate_tracker.get_wait_time.assert_called_once_with(
"ConcreteSearchEngine"
)
def test_record_retry_outcome_success(self):
"""Test _record_retry_outcome records successful outcome."""
engine = ConcreteSearchEngine(programmatic_mode=True)
engine.rate_tracker = Mock()
engine._last_wait_time = 1.0
engine._last_results_count = 5
mock_outcome = Mock()
mock_outcome.failed = False
mock_retry_state = Mock()
mock_retry_state.outcome = mock_outcome
mock_retry_state.attempt_number = 1
engine._record_retry_outcome(mock_retry_state)
engine.rate_tracker.record_outcome.assert_called_once()
# Check call was made with expected arguments
call_args = engine.rate_tracker.record_outcome.call_args
# Engine type is first positional arg
assert call_args[0][0] == "ConcreteSearchEngine"
# Wait time is second positional arg
assert call_args[0][1] == 1.0
def test_record_retry_outcome_failure(self):
"""Test _record_retry_outcome records failed outcome."""
engine = ConcreteSearchEngine(programmatic_mode=True)
engine.rate_tracker = Mock()
engine._last_wait_time = 2.0
mock_outcome = Mock()
mock_outcome.failed = True
mock_retry_state = Mock()
mock_retry_state.outcome = mock_outcome
mock_retry_state.attempt_number = 2
engine._record_retry_outcome(mock_retry_state)
engine.rate_tracker.record_outcome.assert_called_once()
# Check call was made with expected arguments
call_args = engine.rate_tracker.record_outcome.call_args
# Engine type is first positional arg
assert call_args[0][0] == "ConcreteSearchEngine"
# Wait time is second positional arg
assert call_args[0][1] == 2.0
def test_run_with_rate_limit_error_disabled(self):
"""Test run handles RateLimitError when rate limiting disabled."""
class RateLimitedEngine(ConcreteSearchEngine):
def _get_previews(self, query):
raise RateLimitError("Rate limited")
engine = RateLimitedEngine(programmatic_mode=True)
engine.rate_tracker.enabled = False
results = engine.run("test query")
assert results == []
class TestLLMRelevanceFilter:
"""Tests for LLM relevance filtering behavior."""
def test_llm_filter_disabled_by_default(self):
"""Test LLM filter is disabled by default."""
mock_llm = Mock()
engine = ConcreteSearchEngine(llm=mock_llm, programmatic_mode=True)
engine._mock_previews = [
{"title": "A", "snippet": "Content"},
{"title": "B", "snippet": "Content"},
]
# Without enable_llm_relevance_filter set, should return all previews
results = engine.run("query")
mock_llm.invoke.assert_not_called()
assert len(results) == 2
def test_llm_filter_enabled(self):
"""Test LLM filter when enabled."""
mock_llm = _make_structured_llm([0])
engine = ConcreteSearchEngine(llm=mock_llm, programmatic_mode=True)
engine.enable_llm_relevance_filter = True
engine._mock_previews = [
{"title": "A", "snippet": "Content A", "url": "https://a.com"},
{"title": "B", "snippet": "Content B", "url": "https://b.com"},
]
results = engine.run("query")
mock_llm.invoke.assert_called_once()
assert len(results) == 1