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
324 lines
11 KiB
Python
324 lines
11 KiB
Python
"""
|
|
Protocol-based interface tests for SettingsManager.
|
|
|
|
These tests define and verify the contract that SettingsManager must fulfill
|
|
for consumers like rag_routes.py. Using Python's Protocol (PEP 544) enables:
|
|
- Static type checking with mypy to catch wrong imports at development time
|
|
- Runtime verification with @runtime_checkable
|
|
- Clear documentation of required interface
|
|
|
|
This prevents regressions like issue #1877 where the wrong SettingsManager
|
|
was imported, causing 'object has no attribute' errors at runtime.
|
|
"""
|
|
|
|
from typing import Any, Protocol, runtime_checkable
|
|
from unittest.mock import Mock
|
|
|
|
|
|
@runtime_checkable
|
|
class RagRoutesSettingsProtocol(Protocol):
|
|
"""
|
|
Protocol defining the SettingsManager interface required by rag_routes.py.
|
|
|
|
This protocol specifies the exact methods that rag_routes.py calls on
|
|
SettingsManager. Any class used as a SettingsManager in rag_routes.py
|
|
MUST implement all these methods.
|
|
|
|
Usage in rag_routes.py:
|
|
- get_setting(): Called throughout for retrieving settings
|
|
- get_bool_setting(): Called at lines 133, 2185, 2309 for boolean settings
|
|
- get_settings_snapshot(): Called at line 2193 for background thread operations
|
|
"""
|
|
|
|
def get_setting(
|
|
self, key: str, default: Any = None, check_env: bool = True
|
|
) -> Any:
|
|
"""Retrieve a setting value by key."""
|
|
...
|
|
|
|
def get_bool_setting(self, key: str, default: bool = False) -> bool:
|
|
"""Retrieve a setting value as a boolean."""
|
|
...
|
|
|
|
def get_settings_snapshot(self) -> dict[str, Any]:
|
|
"""Get a snapshot of all settings as a flat dictionary."""
|
|
...
|
|
|
|
|
|
class TestSettingsManagerProtocolCompliance:
|
|
"""
|
|
Tests verifying SettingsManager implements the required protocol.
|
|
|
|
These tests use Python's @runtime_checkable Protocol to verify that
|
|
the SettingsManager class implements all required methods with the
|
|
correct signatures.
|
|
"""
|
|
|
|
def test_settings_manager_implements_protocol(self):
|
|
"""
|
|
Verify settings.manager.SettingsManager implements protocol.
|
|
|
|
This is the single unified SettingsManager used throughout the app.
|
|
"""
|
|
from local_deep_research.settings.manager import (
|
|
SettingsManager,
|
|
)
|
|
|
|
manager = SettingsManager()
|
|
|
|
# Runtime protocol check - this verifies the class has all required methods
|
|
assert isinstance(manager, RagRoutesSettingsProtocol), (
|
|
"settings.manager.SettingsManager must implement "
|
|
"RagRoutesSettingsProtocol. Missing methods will cause runtime errors "
|
|
"in rag_routes.py. See issue #1877."
|
|
)
|
|
|
|
|
|
class TestSettingsManagerImportIdentity:
|
|
"""
|
|
Tests verifying the correct SettingsManager is imported in rag_routes.py.
|
|
|
|
These tests check that rag_routes.py imports SettingsManager from
|
|
settings.manager (the unified implementation).
|
|
"""
|
|
|
|
def test_rag_routes_imports_correct_settings_manager(self):
|
|
"""
|
|
Verify rag_routes.py imports SettingsManager from settings.manager.
|
|
|
|
This test checks the actual imported class identity, ensuring that
|
|
rag_routes.py uses the correct SettingsManager that has all required
|
|
methods (get_bool_setting, get_settings_snapshot).
|
|
"""
|
|
from local_deep_research.research_library.routes import rag_routes
|
|
from local_deep_research.settings.manager import (
|
|
SettingsManager as ExpectedManager,
|
|
)
|
|
|
|
# Get the SettingsManager class that rag_routes is using
|
|
# It should be imported at module level or used in functions
|
|
assert hasattr(rag_routes, "SettingsManager"), (
|
|
"rag_routes.py should have SettingsManager available. "
|
|
"Check the import statements."
|
|
)
|
|
|
|
# Verify it's the correct class
|
|
assert rag_routes.SettingsManager is ExpectedManager, (
|
|
f"rag_routes.py imports SettingsManager from wrong module. "
|
|
f"Expected: {ExpectedManager.__module__}.{ExpectedManager.__name__}, "
|
|
f"Got: {rag_routes.SettingsManager.__module__}.{rag_routes.SettingsManager.__name__}. "
|
|
f"This will cause 'has no attribute' errors. See issue #1877."
|
|
)
|
|
|
|
def test_rag_routes_settings_manager_has_required_methods(self):
|
|
"""
|
|
Verify the SettingsManager in rag_routes has all methods it calls.
|
|
|
|
This is a defense-in-depth test that checks the imported class
|
|
has all the methods that rag_routes.py actually calls.
|
|
"""
|
|
from local_deep_research.research_library.routes import rag_routes
|
|
|
|
manager_class = rag_routes.SettingsManager
|
|
manager = manager_class()
|
|
|
|
# Methods called in rag_routes.py
|
|
required_methods = [
|
|
("get_setting", True), # (method_name, is_callable)
|
|
("get_bool_setting", True),
|
|
("get_settings_snapshot", True),
|
|
]
|
|
|
|
for method_name, should_be_callable in required_methods:
|
|
assert hasattr(manager, method_name), (
|
|
f"rag_routes.py's SettingsManager missing '{method_name}'. "
|
|
f"This method is called in rag_routes.py and will cause "
|
|
f"AttributeError at runtime. See issue #1877."
|
|
)
|
|
|
|
if should_be_callable:
|
|
attr = getattr(manager, method_name)
|
|
assert callable(attr), (
|
|
f"rag_routes.py's SettingsManager.{method_name} is not callable. "
|
|
f"Expected a method, got {type(attr)}."
|
|
)
|
|
|
|
|
|
class TestSettingsManagerMethodSignatures:
|
|
"""
|
|
Tests verifying method signatures match expected usage patterns.
|
|
|
|
These tests ensure that the methods not only exist but accept the
|
|
arguments that rag_routes.py passes to them.
|
|
"""
|
|
|
|
def test_get_bool_setting_accepts_key_and_default(self):
|
|
"""
|
|
Verify get_bool_setting accepts (key, default) arguments.
|
|
|
|
rag_routes.py calls: settings.get_bool_setting("key", True)
|
|
"""
|
|
from local_deep_research.settings.manager import (
|
|
SettingsManager,
|
|
)
|
|
|
|
mock_session = Mock()
|
|
mock_query = Mock()
|
|
mock_query.filter.return_value.all.return_value = []
|
|
mock_session.query.return_value = mock_query
|
|
|
|
manager = SettingsManager(db_session=mock_session)
|
|
|
|
# This should not raise TypeError
|
|
result = manager.get_bool_setting(
|
|
"research_library.auto_index_enabled", True
|
|
)
|
|
|
|
assert isinstance(result, bool)
|
|
|
|
def test_get_settings_snapshot_returns_mutable_dict(self):
|
|
"""
|
|
Verify get_settings_snapshot returns a dict that can be modified.
|
|
|
|
rag_routes.py modifies the snapshot: snapshot["_username"] = username
|
|
"""
|
|
from local_deep_research.settings.manager import (
|
|
SettingsManager,
|
|
)
|
|
|
|
mock_session = Mock()
|
|
mock_query = Mock()
|
|
mock_query.all.return_value = []
|
|
mock_session.query.return_value = mock_query
|
|
|
|
manager = SettingsManager(db_session=mock_session)
|
|
snapshot = manager.get_settings_snapshot()
|
|
|
|
# Should be a dict
|
|
assert isinstance(snapshot, dict)
|
|
|
|
# Should be mutable (rag_routes.py adds keys to it)
|
|
snapshot["_username"] = "testuser"
|
|
snapshot["_db_password"] = "testpass"
|
|
|
|
assert snapshot["_username"] == "testuser"
|
|
assert snapshot["_db_password"] == "testpass"
|
|
|
|
def test_get_setting_accepts_check_env_parameter(self):
|
|
"""
|
|
Verify get_setting accepts check_env parameter.
|
|
|
|
rag_routes.py calls: settings.get_setting("key", default, check_env=True)
|
|
"""
|
|
from local_deep_research.settings.manager import (
|
|
SettingsManager,
|
|
)
|
|
|
|
mock_session = Mock()
|
|
mock_query = Mock()
|
|
mock_query.filter.return_value.all.return_value = []
|
|
mock_session.query.return_value = mock_query
|
|
|
|
manager = SettingsManager(db_session=mock_session)
|
|
|
|
# These should not raise TypeError
|
|
result1 = manager.get_setting("some.key", "default", check_env=True)
|
|
result2 = manager.get_setting("some.key", "default", check_env=False)
|
|
|
|
# Should return the default when key not found
|
|
assert result1 == "default"
|
|
assert result2 == "default"
|
|
|
|
|
|
class TestSettingsManagerHasAllMethods:
|
|
"""
|
|
Tests verifying that the unified SettingsManager has all required methods.
|
|
"""
|
|
|
|
def test_settings_manager_has_all_methods(self):
|
|
"""Verify settings.manager.SettingsManager has all expected methods."""
|
|
from local_deep_research.settings.manager import (
|
|
SettingsManager,
|
|
)
|
|
|
|
manager = SettingsManager()
|
|
|
|
expected_methods = [
|
|
"get_setting",
|
|
"get_bool_setting",
|
|
"get_settings_snapshot",
|
|
"get_all_settings",
|
|
"set_setting",
|
|
"delete_setting",
|
|
]
|
|
|
|
for method in expected_methods:
|
|
assert hasattr(manager, method), (
|
|
f"settings.manager.SettingsManager should have {method}"
|
|
)
|
|
|
|
|
|
class TestProtocolUsageInTypeChecking:
|
|
"""
|
|
Tests demonstrating how to use the protocol for type checking.
|
|
|
|
These tests show patterns that can be used elsewhere in the codebase
|
|
to ensure type safety when working with SettingsManager.
|
|
"""
|
|
|
|
def test_function_accepting_protocol(self):
|
|
"""
|
|
Demonstrate a function that accepts any SettingsManager implementing protocol.
|
|
|
|
This pattern can be used in rag_routes.py to ensure type safety:
|
|
|
|
def get_rag_service(settings: RagRoutesSettingsProtocol) -> RAGService:
|
|
enabled = settings.get_bool_setting("auto_index", True)
|
|
...
|
|
"""
|
|
from local_deep_research.settings.manager import (
|
|
SettingsManager,
|
|
)
|
|
|
|
def requires_protocol(settings: RagRoutesSettingsProtocol) -> bool:
|
|
"""Function that requires the protocol."""
|
|
return settings.get_bool_setting("test.key", False)
|
|
|
|
mock_session = Mock()
|
|
mock_query = Mock()
|
|
mock_query.filter.return_value.all.return_value = []
|
|
mock_session.query.return_value = mock_query
|
|
|
|
manager = SettingsManager(db_session=mock_session)
|
|
|
|
# This should work because SettingsManager implements the protocol
|
|
result = requires_protocol(manager)
|
|
assert isinstance(result, bool)
|
|
|
|
def test_protocol_enables_mock_verification(self):
|
|
"""
|
|
Demonstrate using protocol to create properly-shaped mocks.
|
|
|
|
When mocking SettingsManager, the protocol ensures mocks have
|
|
all required methods.
|
|
"""
|
|
|
|
def create_mock_settings() -> RagRoutesSettingsProtocol:
|
|
"""Create a mock that satisfies the protocol."""
|
|
mock = Mock(spec=RagRoutesSettingsProtocol)
|
|
mock.get_setting.return_value = "test_value"
|
|
mock.get_bool_setting.return_value = True
|
|
mock.get_settings_snapshot.return_value = {"key": "value"}
|
|
return mock
|
|
|
|
mock_settings = create_mock_settings()
|
|
|
|
# Verify the mock satisfies the protocol
|
|
assert hasattr(mock_settings, "get_setting")
|
|
assert hasattr(mock_settings, "get_bool_setting")
|
|
assert hasattr(mock_settings, "get_settings_snapshot")
|
|
|
|
# Use the mock
|
|
assert mock_settings.get_bool_setting("test", False) is True
|
|
assert mock_settings.get_settings_snapshot() == {"key": "value"}
|