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
275 lines
10 KiB
Python
275 lines
10 KiB
Python
"""Tests for the hardcoded engine registry.
|
|
|
|
Validates that registry entries are consistent with the security whitelist
|
|
and with the default settings JSON files.
|
|
"""
|
|
|
|
import json
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from local_deep_research.security.module_whitelist import (
|
|
ALLOWED_CLASS_NAMES,
|
|
ALLOWED_MODULE_PATHS,
|
|
)
|
|
from local_deep_research.web_search_engines.engine_registry import (
|
|
ENGINE_REGISTRY,
|
|
EngineEntry,
|
|
get_engine_entry,
|
|
)
|
|
|
|
|
|
class TestRegistryIntegrity:
|
|
"""All registry entries must be consistent with the security whitelist."""
|
|
|
|
def test_all_module_paths_are_relative(self):
|
|
"""Every module_path must start with '.' (relative import)."""
|
|
for name, entry in ENGINE_REGISTRY.items():
|
|
assert entry.module_path.startswith("."), (
|
|
f"Engine '{name}' has non-relative module_path: {entry.module_path}"
|
|
)
|
|
|
|
def test_all_module_paths_in_whitelist(self):
|
|
"""Every module_path must be in ALLOWED_MODULE_PATHS."""
|
|
for name, entry in ENGINE_REGISTRY.items():
|
|
assert entry.module_path in ALLOWED_MODULE_PATHS, (
|
|
f"Engine '{name}' module_path {entry.module_path!r} "
|
|
f"not in ALLOWED_MODULE_PATHS"
|
|
)
|
|
|
|
def test_all_class_names_in_whitelist(self):
|
|
"""Every class_name must be in ALLOWED_CLASS_NAMES."""
|
|
for name, entry in ENGINE_REGISTRY.items():
|
|
assert entry.class_name in ALLOWED_CLASS_NAMES, (
|
|
f"Engine '{name}' class_name {entry.class_name!r} "
|
|
f"not in ALLOWED_CLASS_NAMES"
|
|
)
|
|
|
|
def test_full_search_fields_consistent(self):
|
|
"""full_search_module and full_search_class must both be set or both be None."""
|
|
for name, entry in ENGINE_REGISTRY.items():
|
|
has_module = entry.full_search_module is not None
|
|
has_class = entry.full_search_class is not None
|
|
assert has_module == has_class, (
|
|
f"Engine '{name}' has inconsistent full_search fields: "
|
|
f"module={entry.full_search_module}, class={entry.full_search_class}"
|
|
)
|
|
|
|
def test_full_search_module_paths_in_whitelist(self):
|
|
"""full_search_module values must be in ALLOWED_MODULE_PATHS."""
|
|
for name, entry in ENGINE_REGISTRY.items():
|
|
if entry.full_search_module:
|
|
assert entry.full_search_module in ALLOWED_MODULE_PATHS, (
|
|
f"Engine '{name}' full_search_module "
|
|
f"{entry.full_search_module!r} not in ALLOWED_MODULE_PATHS"
|
|
)
|
|
|
|
def test_full_search_class_names_in_whitelist(self):
|
|
"""full_search_class values must be in ALLOWED_CLASS_NAMES."""
|
|
for name, entry in ENGINE_REGISTRY.items():
|
|
if entry.full_search_class:
|
|
assert entry.full_search_class in ALLOWED_CLASS_NAMES, (
|
|
f"Engine '{name}' full_search_class "
|
|
f"{entry.full_search_class!r} not in ALLOWED_CLASS_NAMES"
|
|
)
|
|
|
|
|
|
class TestRegistryCoverage:
|
|
"""Registry should cover all engines that were previously in settings JSON."""
|
|
|
|
@pytest.fixture()
|
|
def default_settings(self):
|
|
"""Load the default_settings.json file."""
|
|
path = (
|
|
Path(__file__).resolve().parents[2]
|
|
/ "src"
|
|
/ "local_deep_research"
|
|
/ "defaults"
|
|
/ "default_settings.json"
|
|
)
|
|
with open(path) as f:
|
|
return json.load(f)
|
|
|
|
def test_covers_all_web_engines_in_defaults(self, default_settings):
|
|
"""Registry should have entries for all web engines in default_settings.json."""
|
|
engine_names = set()
|
|
for key in default_settings:
|
|
if key.startswith("search.engine.web."):
|
|
parts = key.split(".")
|
|
if len(parts) >= 4:
|
|
engine_names.add(parts[3])
|
|
|
|
for name in engine_names:
|
|
assert name in ENGINE_REGISTRY, (
|
|
f"Engine '{name}' from default_settings.json missing from registry"
|
|
)
|
|
|
|
def test_covers_all_web_engines_in_per_engine_settings(self):
|
|
"""Registry should have entries for all engines with per-engine settings files."""
|
|
defaults_dir = (
|
|
Path(__file__).resolve().parents[2]
|
|
/ "src"
|
|
/ "local_deep_research"
|
|
/ "defaults"
|
|
)
|
|
# Runtime-registered engines that are intentionally not in the registry
|
|
runtime_engines = {"library", "collection"}
|
|
|
|
engine_names = set()
|
|
for settings_file in sorted(defaults_dir.glob("settings_*.json")):
|
|
with open(settings_file) as f:
|
|
data = json.load(f)
|
|
for key in data:
|
|
if key.startswith("search.engine.web."):
|
|
parts = key.split(".")
|
|
if len(parts) >= 4:
|
|
engine_names.add(parts[3])
|
|
|
|
for name in engine_names - runtime_engines:
|
|
assert name in ENGINE_REGISTRY, (
|
|
f"Engine '{name}' from per-engine settings file missing from registry"
|
|
)
|
|
|
|
def test_no_settings_files_contain_module_paths(self):
|
|
"""Settings files should not contain module_path/class_name (now in registry)."""
|
|
defaults_dir = (
|
|
Path(__file__).resolve().parents[2]
|
|
/ "src"
|
|
/ "local_deep_research"
|
|
/ "defaults"
|
|
)
|
|
for settings_file in sorted(defaults_dir.glob("*.json")):
|
|
with open(settings_file) as f:
|
|
content = f.read()
|
|
for field in ("module_path", "class_name"):
|
|
assert field not in content, (
|
|
f"{settings_file.name} still contains '{field}' — "
|
|
f"these should be in engine_registry.py instead"
|
|
)
|
|
|
|
def test_removed_meta_engines_not_in_registry(self):
|
|
"""The removed meta engines must not reappear in the registry."""
|
|
assert "auto" not in ENGINE_REGISTRY
|
|
assert "meta" not in ENGINE_REGISTRY
|
|
assert "parallel" not in ENGINE_REGISTRY
|
|
assert "parallel_scientific" not in ENGINE_REGISTRY
|
|
|
|
|
|
class TestEngineEntryDataclass:
|
|
"""Test the EngineEntry dataclass."""
|
|
|
|
def test_frozen(self):
|
|
"""EngineEntry should be immutable."""
|
|
entry = EngineEntry(
|
|
module_path=".engines.test",
|
|
class_name="TestEngine",
|
|
)
|
|
with pytest.raises(AttributeError):
|
|
entry.module_path = "changed"
|
|
|
|
def test_optional_full_search_defaults_to_none(self):
|
|
"""full_search fields default to None."""
|
|
entry = EngineEntry(
|
|
module_path=".engines.test",
|
|
class_name="TestEngine",
|
|
)
|
|
assert entry.full_search_module is None
|
|
assert entry.full_search_class is None
|
|
|
|
def test_full_search_fields_set(self):
|
|
"""full_search fields can be set."""
|
|
entry = EngineEntry(
|
|
module_path=".engines.test",
|
|
class_name="TestEngine",
|
|
full_search_module=".engines.full_search",
|
|
full_search_class="FullSearchResults",
|
|
)
|
|
assert entry.full_search_module == ".engines.full_search"
|
|
assert entry.full_search_class == "FullSearchResults"
|
|
|
|
|
|
class TestGetEngineEntry:
|
|
"""Test the get_engine_entry helper."""
|
|
|
|
def test_returns_entry_for_known_engine(self):
|
|
"""Should return EngineEntry for known engine names."""
|
|
entry = get_engine_entry("brave")
|
|
assert entry is not None
|
|
assert entry.class_name == "BraveSearchEngine"
|
|
|
|
def test_returns_none_for_unknown_engine(self):
|
|
"""Should return None for unknown engine names."""
|
|
assert get_engine_entry("nonexistent_engine") is None
|
|
|
|
def test_returns_none_for_runtime_engines(self):
|
|
"""Runtime-registered engines (library, collection_*) are not in registry."""
|
|
assert get_engine_entry("library") is None
|
|
assert get_engine_entry("collection_1") is None
|
|
|
|
|
|
class TestSearchConfigInjection:
|
|
"""Test that search_config() injects registry data."""
|
|
|
|
def test_search_config_injects_module_path(self):
|
|
"""search_config() should inject module_path from registry."""
|
|
from unittest.mock import patch
|
|
|
|
from local_deep_research.web_search_engines.search_engines_config import (
|
|
search_config,
|
|
)
|
|
|
|
def mock_get_setting(key, default, **kwargs):
|
|
if key == "search.engine.web":
|
|
return {
|
|
"brave.requires_api_key": True,
|
|
"brave.api_key": "test-key",
|
|
}
|
|
return default
|
|
|
|
with (
|
|
patch(
|
|
"local_deep_research.web_search_engines.search_engines_config._get_setting",
|
|
side_effect=mock_get_setting,
|
|
),
|
|
patch(
|
|
"local_deep_research.web_search_engines.retriever_registry.retriever_registry"
|
|
) as mock_registry,
|
|
):
|
|
mock_registry.list_registered.return_value = []
|
|
result = search_config()
|
|
|
|
assert "brave" in result
|
|
assert result["brave"]["module_path"] == ".engines.search_engine_brave"
|
|
assert result["brave"]["class_name"] == "BraveSearchEngine"
|
|
assert result["brave"]["full_search_module"] == ".engines.full_search"
|
|
assert result["brave"]["full_search_class"] == "FullSearchResults"
|
|
|
|
def test_removed_meta_engines_absent_from_search_config(self):
|
|
"""search_config() should not contain the removed meta engines."""
|
|
from unittest.mock import patch
|
|
|
|
from local_deep_research.web_search_engines.search_engines_config import (
|
|
search_config,
|
|
)
|
|
|
|
def mock_get_setting(key, default, **kwargs):
|
|
return default
|
|
|
|
with (
|
|
patch(
|
|
"local_deep_research.web_search_engines.search_engines_config._get_setting",
|
|
side_effect=mock_get_setting,
|
|
),
|
|
patch(
|
|
"local_deep_research.web_search_engines.retriever_registry.retriever_registry"
|
|
) as mock_registry,
|
|
):
|
|
mock_registry.list_registered.return_value = []
|
|
result = search_config()
|
|
|
|
assert "auto" not in result
|
|
assert "meta" not in result
|
|
assert "parallel" not in result
|
|
assert "parallel_scientific" not in result
|