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

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})