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
834 lines
29 KiB
Python
834 lines
29 KiB
Python
"""
|
|
Tests for web_search_engines/search_engine_factory.py
|
|
|
|
Tests cover:
|
|
- create_search_engine function
|
|
- get_search function
|
|
- _create_full_search_wrapper function
|
|
- Removed meta engines ('auto', 'parallel', ...) rejected with ValueError
|
|
- API key and LLM requirements
|
|
- LLM relevance filter settings
|
|
"""
|
|
|
|
import pytest
|
|
from unittest.mock import Mock, patch
|
|
|
|
|
|
class TestRemovedMetaEnginesRejected:
|
|
"""The 'auto'/'meta'/'parallel' meta engines were removed — the factory
|
|
must reject them like any unknown engine, with a message pointing at
|
|
the removal."""
|
|
|
|
@pytest.mark.parametrize(
|
|
"engine_name", ["auto", "meta", "parallel", "parallel_scientific"]
|
|
)
|
|
def test_removed_engine_raises_value_error(self, engine_name):
|
|
"""Creating a removed meta engine raises ValueError mentioning the removal."""
|
|
from local_deep_research.web_search_engines.search_engine_factory import (
|
|
create_search_engine,
|
|
)
|
|
|
|
with patch(
|
|
"local_deep_research.web_search_engines.search_engine_factory.retriever_registry"
|
|
) as mock_registry:
|
|
mock_registry.get.return_value = None
|
|
|
|
with pytest.raises(ValueError, match="meta engines were removed"):
|
|
create_search_engine(
|
|
engine_name=engine_name,
|
|
llm=Mock(),
|
|
settings_snapshot={"test": "value"},
|
|
)
|
|
|
|
|
|
class TestCreateSearchEngineRetriever:
|
|
"""Tests for create_search_engine with registered retrievers."""
|
|
|
|
def test_create_engine_with_registered_retriever(self):
|
|
"""Test creating engine from registered retriever."""
|
|
mock_retriever = Mock()
|
|
|
|
with patch(
|
|
"local_deep_research.web_search_engines.search_engine_factory.retriever_registry"
|
|
) as mock_registry:
|
|
mock_registry.get.return_value = mock_retriever
|
|
|
|
with patch(
|
|
"local_deep_research.web_search_engines.engines.search_engine_retriever.RetrieverSearchEngine"
|
|
) as mock_class:
|
|
mock_engine = Mock()
|
|
mock_class.return_value = mock_engine
|
|
|
|
from local_deep_research.web_search_engines.search_engine_factory import (
|
|
create_search_engine,
|
|
)
|
|
|
|
create_search_engine(
|
|
engine_name="custom_retriever",
|
|
settings_snapshot={"test": "value"},
|
|
)
|
|
|
|
mock_class.assert_called_once()
|
|
call_kwargs = mock_class.call_args[1]
|
|
assert call_kwargs["retriever"] == mock_retriever
|
|
assert call_kwargs["name"] == "custom_retriever"
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _module_wide_bypass_engine_pdp():
|
|
"""Auto-applied to every test in this module — bypasses the PEP so
|
|
factory tests using mock engine names don't get rejected by the
|
|
egress policy. The PEP itself is exercised in
|
|
tests/security/test_egress_policy.py.
|
|
"""
|
|
from local_deep_research.security.egress.policy import Decision
|
|
|
|
with (
|
|
patch(
|
|
"local_deep_research.security.egress.policy.evaluate_engine",
|
|
return_value=Decision(True, "test_bypass"),
|
|
),
|
|
patch(
|
|
"local_deep_research.security.egress.policy.evaluate_retriever",
|
|
return_value=Decision(True, "test_bypass"),
|
|
),
|
|
):
|
|
yield
|
|
|
|
|
|
class TestCreateSearchEngineRequirements:
|
|
"""Tests for API key and LLM requirements."""
|
|
|
|
def test_missing_settings_snapshot_raises_error(self):
|
|
"""Test that missing settings_snapshot raises RuntimeError."""
|
|
with patch(
|
|
"local_deep_research.web_search_engines.search_engine_factory.retriever_registry"
|
|
) as mock_registry:
|
|
mock_registry.get.return_value = None
|
|
|
|
from local_deep_research.web_search_engines.search_engine_factory import (
|
|
create_search_engine,
|
|
)
|
|
|
|
with pytest.raises(RuntimeError) as exc_info:
|
|
create_search_engine(
|
|
engine_name="test_engine",
|
|
settings_snapshot=None,
|
|
)
|
|
|
|
assert "settings_snapshot is required" in str(exc_info.value)
|
|
|
|
def test_missing_api_key_returns_none(self):
|
|
"""Test that missing required API key returns None."""
|
|
with patch(
|
|
"local_deep_research.web_search_engines.search_engine_factory.retriever_registry"
|
|
) as mock_registry:
|
|
mock_registry.get.return_value = None
|
|
|
|
with patch(
|
|
"local_deep_research.web_search_engines.search_engine_factory.search_config"
|
|
) as mock_config:
|
|
mock_config.return_value = {
|
|
"test_engine": {
|
|
"module_path": ".engines.test",
|
|
"class_name": "TestEngine",
|
|
"requires_api_key": True,
|
|
}
|
|
}
|
|
|
|
from local_deep_research.web_search_engines.search_engine_factory import (
|
|
create_search_engine,
|
|
)
|
|
|
|
result = create_search_engine(
|
|
engine_name="test_engine",
|
|
settings_snapshot={"dummy": "value"}, # Non-empty snapshot
|
|
)
|
|
|
|
assert result is None
|
|
|
|
|
|
class TestDisplayLabelFallback:
|
|
"""Tests for display label fallback functionality."""
|
|
|
|
def test_extract_base_name_from_label(self):
|
|
"""Test extracting base name from display label."""
|
|
engine_name = "🔬 OpenAlex (Scientific)"
|
|
|
|
base_name = None
|
|
if " (" in engine_name and engine_name.endswith(")"):
|
|
parts = engine_name.rsplit(" (", 1)
|
|
if len(parts) == 2:
|
|
before_paren = parts[0]
|
|
space_idx = before_paren.find(" ")
|
|
if space_idx > 0:
|
|
base_name = before_paren[space_idx + 1 :].strip()
|
|
|
|
assert base_name == "OpenAlex"
|
|
|
|
def test_label_without_parentheses(self):
|
|
"""Test label without parentheses format."""
|
|
engine_name = "simple_engine"
|
|
|
|
extracted = None
|
|
if " (" in engine_name and engine_name.endswith(")"):
|
|
extracted = "something"
|
|
|
|
assert extracted is None
|
|
|
|
def test_label_matching_config(self):
|
|
"""Test matching extracted label to config."""
|
|
config = {
|
|
"openalex": {
|
|
"display_name": "OpenAlex",
|
|
"module_path": ".engines.search_engine_openalex",
|
|
"class_name": "OpenAlexSearchEngine",
|
|
}
|
|
}
|
|
|
|
base_name = "OpenAlex"
|
|
matched_key = None
|
|
|
|
for config_key, config_data in config.items():
|
|
if isinstance(config_data, dict):
|
|
display_name = config_data.get("display_name", config_key)
|
|
if display_name == base_name:
|
|
matched_key = config_key
|
|
break
|
|
|
|
assert matched_key == "openalex"
|
|
|
|
|
|
class TestMaxResultsDefault:
|
|
"""Tests for max_results default handling."""
|
|
|
|
def test_max_results_from_settings_dict(self):
|
|
"""Test max_results from settings as dict."""
|
|
settings_snapshot = {"search.max_results": {"value": 25}}
|
|
|
|
max_results = None
|
|
if "search.max_results" in settings_snapshot:
|
|
max_results = (
|
|
settings_snapshot["search.max_results"].get("value", 20)
|
|
if isinstance(settings_snapshot["search.max_results"], dict)
|
|
else settings_snapshot["search.max_results"]
|
|
)
|
|
|
|
assert max_results == 25
|
|
|
|
def test_max_results_from_settings_direct(self):
|
|
"""Test max_results from settings as direct value."""
|
|
settings_snapshot = {"search.max_results": 30}
|
|
|
|
max_results = None
|
|
if "search.max_results" in settings_snapshot:
|
|
max_results = (
|
|
settings_snapshot["search.max_results"].get("value", 20)
|
|
if isinstance(settings_snapshot["search.max_results"], dict)
|
|
else settings_snapshot["search.max_results"]
|
|
)
|
|
|
|
assert max_results == 30
|
|
|
|
def test_max_results_default(self):
|
|
"""Test max_results default when not in settings."""
|
|
settings_snapshot = {}
|
|
kwargs = {}
|
|
|
|
if "max_results" not in kwargs:
|
|
if settings_snapshot and "search.max_results" in settings_snapshot:
|
|
max_results = settings_snapshot["search.max_results"]
|
|
else:
|
|
max_results = 20
|
|
kwargs["max_results"] = max_results
|
|
|
|
assert kwargs["max_results"] == 20
|
|
|
|
|
|
class TestGetSearch:
|
|
"""Tests for get_search function."""
|
|
|
|
def test_get_search_basic(self):
|
|
"""Test basic get_search call."""
|
|
mock_llm = Mock()
|
|
mock_engine = Mock()
|
|
mock_engine.run = Mock()
|
|
|
|
with patch(
|
|
"local_deep_research.web_search_engines.search_engine_factory.create_search_engine"
|
|
) as mock_create:
|
|
mock_create.return_value = mock_engine
|
|
|
|
from local_deep_research.web_search_engines.search_engine_factory import (
|
|
get_search,
|
|
)
|
|
|
|
result = get_search(
|
|
search_tool="duckduckgo",
|
|
llm_instance=mock_llm,
|
|
max_results=10,
|
|
settings_snapshot={"test": "value"},
|
|
)
|
|
|
|
mock_create.assert_called_once()
|
|
assert result == mock_engine
|
|
|
|
def test_get_search_with_duckduckgo_params(self):
|
|
"""Test get_search with DuckDuckGo specific params."""
|
|
mock_llm = Mock()
|
|
mock_engine = Mock()
|
|
|
|
with patch(
|
|
"local_deep_research.web_search_engines.search_engine_factory.create_search_engine"
|
|
) as mock_create:
|
|
mock_create.return_value = mock_engine
|
|
|
|
from local_deep_research.web_search_engines.search_engine_factory import (
|
|
get_search,
|
|
)
|
|
|
|
get_search(
|
|
search_tool="duckduckgo",
|
|
llm_instance=mock_llm,
|
|
max_results=20,
|
|
region="uk",
|
|
safe_search=False,
|
|
search_snippets_only=True,
|
|
settings_snapshot={"test": "value"},
|
|
)
|
|
|
|
call_kwargs = mock_create.call_args[1]
|
|
assert call_kwargs["region"] == "uk"
|
|
assert call_kwargs["safe_search"] is False
|
|
assert call_kwargs["use_full_search"] is False
|
|
|
|
def test_get_search_with_serpapi_params(self):
|
|
"""Test get_search with SerpAPI specific params."""
|
|
mock_llm = Mock()
|
|
mock_engine = Mock()
|
|
|
|
with patch(
|
|
"local_deep_research.web_search_engines.search_engine_factory.create_search_engine"
|
|
) as mock_create:
|
|
mock_create.return_value = mock_engine
|
|
|
|
from local_deep_research.web_search_engines.search_engine_factory import (
|
|
get_search,
|
|
)
|
|
|
|
get_search(
|
|
search_tool="serpapi",
|
|
llm_instance=mock_llm,
|
|
max_results=15,
|
|
time_period="m",
|
|
search_language="Spanish",
|
|
settings_snapshot={"test": "value"},
|
|
)
|
|
|
|
call_kwargs = mock_create.call_args[1]
|
|
assert call_kwargs["time_period"] == "m"
|
|
assert call_kwargs["search_language"] == "Spanish"
|
|
|
|
def test_get_search_returns_none(self):
|
|
"""Test get_search when engine creation fails."""
|
|
mock_llm = Mock()
|
|
|
|
with patch(
|
|
"local_deep_research.web_search_engines.search_engine_factory.create_search_engine"
|
|
) as mock_create:
|
|
mock_create.return_value = None
|
|
|
|
from local_deep_research.web_search_engines.search_engine_factory import (
|
|
get_search,
|
|
)
|
|
|
|
result = get_search(
|
|
search_tool="nonexistent",
|
|
llm_instance=mock_llm,
|
|
settings_snapshot={"test": "value"},
|
|
)
|
|
|
|
assert result is None
|
|
|
|
def test_get_search_adds_region_params(self):
|
|
"""get_search adds region parameters for supported engines."""
|
|
from local_deep_research.web_search_engines.search_engine_factory import (
|
|
get_search,
|
|
)
|
|
|
|
mock_llm = Mock()
|
|
|
|
settings_snapshot = {
|
|
"search.max_results": {"value": 10},
|
|
}
|
|
|
|
with patch(
|
|
"local_deep_research.web_search_engines.search_engine_factory.create_search_engine"
|
|
) as mock_create:
|
|
mock_create.return_value = Mock()
|
|
|
|
get_search(
|
|
search_tool="duckduckgo",
|
|
llm_instance=mock_llm,
|
|
max_results=10,
|
|
region="uk",
|
|
safe_search=True,
|
|
settings_snapshot=settings_snapshot,
|
|
)
|
|
|
|
# Check that region was passed
|
|
call_kwargs = mock_create.call_args[1]
|
|
assert call_kwargs.get("region") == "uk"
|
|
assert call_kwargs.get("safe_search") is True
|
|
|
|
def test_get_search_adds_language_params(self):
|
|
"""get_search adds language parameters for supported engines."""
|
|
from local_deep_research.web_search_engines.search_engine_factory import (
|
|
get_search,
|
|
)
|
|
|
|
mock_llm = Mock()
|
|
|
|
settings_snapshot = {
|
|
"search.max_results": {"value": 10},
|
|
}
|
|
|
|
with patch(
|
|
"local_deep_research.web_search_engines.search_engine_factory.create_search_engine"
|
|
) as mock_create:
|
|
mock_create.return_value = Mock()
|
|
|
|
get_search(
|
|
search_tool="brave",
|
|
llm_instance=mock_llm,
|
|
max_results=10,
|
|
search_language="German",
|
|
settings_snapshot=settings_snapshot,
|
|
)
|
|
|
|
call_kwargs = mock_create.call_args[1]
|
|
assert call_kwargs.get("search_language") == "German"
|
|
|
|
|
|
class TestFullSearchWrapper:
|
|
"""Tests for _create_full_search_wrapper function."""
|
|
|
|
def test_wrapper_returns_base_on_missing_config(self):
|
|
"""Test wrapper returns base engine when config missing."""
|
|
mock_base_engine = Mock()
|
|
config = {}
|
|
engine_name = "test_engine"
|
|
|
|
if engine_name not in config:
|
|
result = mock_base_engine
|
|
else:
|
|
result = None
|
|
|
|
assert result == mock_base_engine
|
|
|
|
def test_wrapper_config_extraction(self):
|
|
"""Test config extraction from settings snapshot."""
|
|
settings_snapshot = {
|
|
"search.engine.web.serpapi.api_key": {"value": "test-key"},
|
|
"search.engine.web.serpapi.class_name": {"value": "SerpApiSearch"},
|
|
}
|
|
|
|
web_engines = {}
|
|
for key, value in settings_snapshot.items():
|
|
if key.startswith("search.engine.web."):
|
|
parts = key.split(".")
|
|
if len(parts) >= 4:
|
|
engine_name = parts[3]
|
|
if engine_name not in web_engines:
|
|
web_engines[engine_name] = {}
|
|
remaining_key = (
|
|
".".join(parts[4:]) if len(parts) > 4 else ""
|
|
)
|
|
if remaining_key:
|
|
web_engines[engine_name][remaining_key] = (
|
|
value.get("value")
|
|
if isinstance(value, dict)
|
|
else value
|
|
)
|
|
|
|
assert "serpapi" in web_engines
|
|
assert web_engines["serpapi"]["api_key"] == "test-key"
|
|
|
|
|
|
class TestParameterFiltering:
|
|
"""Tests for parameter filtering logic."""
|
|
|
|
def test_filter_unsupported_params(self):
|
|
"""Test filtering of unsupported parameters."""
|
|
engine_init_params = [
|
|
"self",
|
|
"max_results",
|
|
"api_key",
|
|
"settings_snapshot",
|
|
]
|
|
all_params = {
|
|
"max_results": 20,
|
|
"api_key": "test-key",
|
|
"unsupported_param": "value",
|
|
"another_unsupported": "value2",
|
|
}
|
|
|
|
filtered_params = {
|
|
k: v for k, v in all_params.items() if k in engine_init_params[1:]
|
|
}
|
|
|
|
assert "max_results" in filtered_params
|
|
assert "api_key" in filtered_params
|
|
assert "unsupported_param" not in filtered_params
|
|
assert "another_unsupported" not in filtered_params
|
|
|
|
def test_add_settings_snapshot_to_params(self):
|
|
"""Test adding settings_snapshot to params if accepted."""
|
|
engine_init_params = ["self", "max_results", "settings_snapshot"]
|
|
filtered_params = {"max_results": 20}
|
|
settings_snapshot = {"test": "value"}
|
|
|
|
if "settings_snapshot" in engine_init_params[1:] and settings_snapshot:
|
|
filtered_params["settings_snapshot"] = settings_snapshot
|
|
|
|
assert "settings_snapshot" in filtered_params
|
|
|
|
def test_add_programmatic_mode_to_params(self):
|
|
"""Test adding programmatic_mode to params if accepted."""
|
|
engine_init_params = ["self", "max_results", "programmatic_mode"]
|
|
filtered_params = {"max_results": 20}
|
|
programmatic_mode = True
|
|
|
|
if "programmatic_mode" in engine_init_params[1:]:
|
|
filtered_params["programmatic_mode"] = programmatic_mode
|
|
|
|
assert filtered_params["programmatic_mode"] is True
|
|
|
|
|
|
def _make_engine_class(*param_names):
|
|
"""Create a real class with a controlled __init__ signature.
|
|
|
|
The code under test calls inspect.signature(cls.__init__),
|
|
so we need a real class whose __init__ has the desired parameters.
|
|
The engine is instantiated with **filtered_params (kwargs style),
|
|
so the __init__ must accept **kwargs. We use functools.wraps to
|
|
preserve the signature for inspect while accepting any kwargs.
|
|
"""
|
|
import inspect as _inspect
|
|
|
|
# Build a signature object that the factory code will inspect
|
|
params = [
|
|
_inspect.Parameter("self", _inspect.Parameter.POSITIONAL_OR_KEYWORD)
|
|
]
|
|
for name in param_names:
|
|
params.append(
|
|
_inspect.Parameter(name, _inspect.Parameter.POSITIONAL_OR_KEYWORD)
|
|
)
|
|
|
|
class _Eng:
|
|
_call_kwargs = None
|
|
|
|
def __init__(self, **kwargs):
|
|
_Eng._call_kwargs = kwargs
|
|
|
|
# Attach the desired signature so inspect.signature(cls.__init__) sees our params
|
|
_Eng.__init__.__signature__ = _inspect.Signature(params)
|
|
|
|
return _Eng
|
|
|
|
|
|
@pytest.fixture
|
|
def _bypass_engine_pdp():
|
|
"""Patch evaluate_engine to allow unknown mock engine names through.
|
|
The PEP is exercised separately in tests/security/test_egress_policy.py;
|
|
factory tests below use mock names (myengine, eng) that aren't in the
|
|
static ENGINE_REGISTRY and would otherwise be rejected as engine_unknown.
|
|
"""
|
|
from local_deep_research.security.egress.policy import Decision
|
|
|
|
with patch(
|
|
"local_deep_research.security.egress.policy.evaluate_engine",
|
|
return_value=Decision(True, "test_bypass"),
|
|
):
|
|
yield
|
|
|
|
|
|
class TestCreateEngineInstantiation:
|
|
"""Tests for the real engine instantiation path (lines 194-339)."""
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _auto_bypass(self, _bypass_engine_pdp):
|
|
"""Auto-apply the PEP bypass to every test in this class."""
|
|
yield
|
|
|
|
def _make_engine_config(self, **overrides):
|
|
"""Build a minimal engine config dict."""
|
|
base = {
|
|
"module_path": "some.module",
|
|
"class_name": "SomeEngine",
|
|
"default_params": {},
|
|
}
|
|
base.update(overrides)
|
|
return base
|
|
|
|
def test_calls_get_safe_module_class(self):
|
|
"""get_safe_module_class is called with module_path and class_name."""
|
|
from local_deep_research.web_search_engines.search_engine_factory import (
|
|
create_search_engine,
|
|
)
|
|
|
|
EngCls = _make_engine_class()
|
|
|
|
with (
|
|
patch(
|
|
"local_deep_research.web_search_engines.search_engine_factory.retriever_registry"
|
|
) as mock_reg,
|
|
patch(
|
|
"local_deep_research.web_search_engines.search_engine_factory.search_config"
|
|
) as mock_sc,
|
|
patch(
|
|
"local_deep_research.web_search_engines.search_engine_factory.get_safe_module_class"
|
|
) as mock_gsmc,
|
|
):
|
|
mock_reg.get.return_value = None
|
|
mock_sc.return_value = {"myengine": self._make_engine_config()}
|
|
mock_gsmc.return_value = EngCls
|
|
|
|
create_search_engine("myengine", settings_snapshot={"x": 1})
|
|
|
|
mock_gsmc.assert_called_once_with("some.module", "SomeEngine")
|
|
|
|
def test_filters_unsupported_params(self):
|
|
"""Parameters not in engine __init__ are filtered out."""
|
|
from local_deep_research.web_search_engines.search_engine_factory import (
|
|
create_search_engine,
|
|
)
|
|
|
|
EngCls = _make_engine_class("max_results")
|
|
|
|
with (
|
|
patch(
|
|
"local_deep_research.web_search_engines.search_engine_factory.retriever_registry"
|
|
) as mock_reg,
|
|
patch(
|
|
"local_deep_research.web_search_engines.search_engine_factory.search_config"
|
|
) as mock_sc,
|
|
patch(
|
|
"local_deep_research.web_search_engines.search_engine_factory.get_safe_module_class"
|
|
) as mock_gsmc,
|
|
):
|
|
mock_reg.get.return_value = None
|
|
mock_sc.return_value = {
|
|
"eng": self._make_engine_config(
|
|
default_params={"max_results": 5, "bogus_param": "nope"}
|
|
)
|
|
}
|
|
mock_gsmc.return_value = EngCls
|
|
|
|
create_search_engine("eng", settings_snapshot={"x": 1})
|
|
|
|
assert "max_results" in EngCls._call_kwargs
|
|
assert "bogus_param" not in EngCls._call_kwargs
|
|
|
|
def test_passes_settings_snapshot(self):
|
|
"""settings_snapshot is passed when engine accepts it."""
|
|
from local_deep_research.web_search_engines.search_engine_factory import (
|
|
create_search_engine,
|
|
)
|
|
|
|
EngCls = _make_engine_class("settings_snapshot")
|
|
snapshot = {"key": "val"}
|
|
|
|
with (
|
|
patch(
|
|
"local_deep_research.web_search_engines.search_engine_factory.retriever_registry"
|
|
) as mock_reg,
|
|
patch(
|
|
"local_deep_research.web_search_engines.search_engine_factory.search_config"
|
|
) as mock_sc,
|
|
patch(
|
|
"local_deep_research.web_search_engines.search_engine_factory.get_safe_module_class"
|
|
) as mock_gsmc,
|
|
):
|
|
mock_reg.get.return_value = None
|
|
mock_sc.return_value = {"eng": self._make_engine_config()}
|
|
mock_gsmc.return_value = EngCls
|
|
|
|
create_search_engine("eng", settings_snapshot=snapshot)
|
|
|
|
assert EngCls._call_kwargs["settings_snapshot"] is snapshot
|
|
|
|
def test_passes_programmatic_mode(self):
|
|
"""programmatic_mode flag is passed when engine accepts it."""
|
|
from local_deep_research.web_search_engines.search_engine_factory import (
|
|
create_search_engine,
|
|
)
|
|
|
|
EngCls = _make_engine_class("programmatic_mode")
|
|
|
|
with (
|
|
patch(
|
|
"local_deep_research.web_search_engines.search_engine_factory.retriever_registry"
|
|
) as mock_reg,
|
|
patch(
|
|
"local_deep_research.web_search_engines.search_engine_factory.search_config"
|
|
) as mock_sc,
|
|
patch(
|
|
"local_deep_research.web_search_engines.search_engine_factory.get_safe_module_class"
|
|
) as mock_gsmc,
|
|
):
|
|
mock_reg.get.return_value = None
|
|
mock_sc.return_value = {"eng": self._make_engine_config()}
|
|
mock_gsmc.return_value = EngCls
|
|
|
|
create_search_engine(
|
|
"eng", settings_snapshot={"x": 1}, programmatic_mode=True
|
|
)
|
|
|
|
assert EngCls._call_kwargs["programmatic_mode"] is True
|
|
|
|
def test_adds_llm_when_requires_llm(self):
|
|
"""LLM is injected when engine config has requires_llm=True."""
|
|
from local_deep_research.web_search_engines.search_engine_factory import (
|
|
create_search_engine,
|
|
)
|
|
|
|
EngCls = _make_engine_class("llm")
|
|
mock_llm = Mock()
|
|
|
|
with (
|
|
patch(
|
|
"local_deep_research.web_search_engines.search_engine_factory.retriever_registry"
|
|
) as mock_reg,
|
|
patch(
|
|
"local_deep_research.web_search_engines.search_engine_factory.search_config"
|
|
) as mock_sc,
|
|
patch(
|
|
"local_deep_research.web_search_engines.search_engine_factory.get_safe_module_class"
|
|
) as mock_gsmc,
|
|
):
|
|
mock_reg.get.return_value = None
|
|
mock_sc.return_value = {
|
|
"eng": self._make_engine_config(requires_llm=True)
|
|
}
|
|
mock_gsmc.return_value = EngCls
|
|
|
|
create_search_engine(
|
|
"eng", llm=mock_llm, settings_snapshot={"x": 1}
|
|
)
|
|
|
|
assert EngCls._call_kwargs["llm"] is mock_llm
|
|
|
|
def test_exception_returns_none(self):
|
|
"""Exception during instantiation returns None."""
|
|
from local_deep_research.web_search_engines.search_engine_factory import (
|
|
create_search_engine,
|
|
)
|
|
|
|
with (
|
|
patch(
|
|
"local_deep_research.web_search_engines.search_engine_factory.retriever_registry"
|
|
) as mock_reg,
|
|
patch(
|
|
"local_deep_research.web_search_engines.search_engine_factory.search_config"
|
|
) as mock_sc,
|
|
patch(
|
|
"local_deep_research.web_search_engines.search_engine_factory.get_safe_module_class"
|
|
) as mock_gsmc,
|
|
):
|
|
mock_reg.get.return_value = None
|
|
mock_sc.return_value = {"eng": self._make_engine_config()}
|
|
mock_gsmc.side_effect = ImportError("module not found")
|
|
|
|
result = create_search_engine("eng", settings_snapshot={"x": 1})
|
|
assert result is None
|
|
|
|
def test_unknown_engine_raises_value_error(self):
|
|
"""Unknown engine name FAILS CLOSED (was silent rewrite to 'auto' — see plan C2)."""
|
|
from local_deep_research.web_search_engines.search_engine_factory import (
|
|
create_search_engine,
|
|
)
|
|
|
|
EngCls = _make_engine_class()
|
|
|
|
with (
|
|
patch(
|
|
"local_deep_research.web_search_engines.search_engine_factory.retriever_registry"
|
|
) as mock_reg,
|
|
patch(
|
|
"local_deep_research.web_search_engines.search_engine_factory.search_config"
|
|
) as mock_sc,
|
|
patch(
|
|
"local_deep_research.web_search_engines.search_engine_factory.get_safe_module_class"
|
|
) as mock_gsmc,
|
|
):
|
|
mock_reg.get.return_value = None
|
|
mock_sc.return_value = {"auto": self._make_engine_config()}
|
|
mock_gsmc.return_value = EngCls
|
|
|
|
with pytest.raises(ValueError, match="Unknown search engine"):
|
|
create_search_engine("nonexistent", settings_snapshot={"x": 1})
|
|
|
|
def test_no_config_raises_value_error(self):
|
|
"""Missing engine in config (and no 'auto') raises ValueError (was silent None — plan C2)."""
|
|
from local_deep_research.web_search_engines.search_engine_factory import (
|
|
create_search_engine,
|
|
)
|
|
|
|
with (
|
|
patch(
|
|
"local_deep_research.web_search_engines.search_engine_factory.retriever_registry"
|
|
) as mock_reg,
|
|
patch(
|
|
"local_deep_research.web_search_engines.search_engine_factory.search_config"
|
|
) as mock_sc,
|
|
):
|
|
mock_reg.get.return_value = None
|
|
mock_sc.return_value = {"other": {}}
|
|
|
|
with pytest.raises(ValueError, match="Unknown search engine"):
|
|
create_search_engine("nonexistent", settings_snapshot={"x": 1})
|
|
|
|
def test_default_params_merged(self):
|
|
"""default_params from config are merged with kwargs."""
|
|
from local_deep_research.web_search_engines.search_engine_factory import (
|
|
create_search_engine,
|
|
)
|
|
|
|
EngCls = _make_engine_class("max_results", "region")
|
|
|
|
with (
|
|
patch(
|
|
"local_deep_research.web_search_engines.search_engine_factory.retriever_registry"
|
|
) as mock_reg,
|
|
patch(
|
|
"local_deep_research.web_search_engines.search_engine_factory.search_config"
|
|
) as mock_sc,
|
|
patch(
|
|
"local_deep_research.web_search_engines.search_engine_factory.get_safe_module_class"
|
|
) as mock_gsmc,
|
|
):
|
|
mock_reg.get.return_value = None
|
|
mock_sc.return_value = {
|
|
"eng": self._make_engine_config(default_params={"region": "us"})
|
|
}
|
|
mock_gsmc.return_value = EngCls
|
|
|
|
create_search_engine(
|
|
"eng", settings_snapshot={"x": 1}, max_results=15
|
|
)
|
|
|
|
# default_params region should be present
|
|
assert EngCls._call_kwargs["region"] == "us"
|
|
# kwargs max_results should be present
|
|
assert EngCls._call_kwargs["max_results"] == 15
|
|
|
|
def test_none_engine_rejected(self):
|
|
"""search.tool='none' raises ValueError instead of silent auto-fallback."""
|
|
from local_deep_research.web_search_engines.search_engine_factory import (
|
|
create_search_engine,
|
|
)
|
|
|
|
with pytest.raises(ValueError, match="search.tool='none'"):
|
|
create_search_engine("none", settings_snapshot={"x": 1})
|