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
1103 lines
37 KiB
Python
1103 lines
37 KiB
Python
"""
|
|
Extended tests for settings/manager.py
|
|
|
|
Tests cover additional functionality not in the main test file:
|
|
- Type conversion edge cases (get_bool_setting, various ui_elements)
|
|
- Environment variable edge cases
|
|
- Import/export settings
|
|
- Version checking
|
|
- Default settings loading
|
|
- create_setting helper
|
|
- WebSocket event emission
|
|
- Nested settings paths
|
|
- Password/sensitive field handling
|
|
"""
|
|
|
|
import os
|
|
import json
|
|
from unittest.mock import Mock, MagicMock, patch
|
|
|
|
|
|
class MockSetting:
|
|
"""Mock Setting model for testing."""
|
|
|
|
def __init__(
|
|
self,
|
|
key,
|
|
value,
|
|
ui_element="text",
|
|
editable=True,
|
|
type=None,
|
|
name=None,
|
|
description=None,
|
|
category=None,
|
|
options=None,
|
|
min_value=None,
|
|
max_value=None,
|
|
step=None,
|
|
visible=True,
|
|
):
|
|
self.key = key
|
|
self.value = value
|
|
self.ui_element = ui_element
|
|
self.editable = editable
|
|
self.type = type or Mock(name="APP")
|
|
self.name = name or key
|
|
self.description = description or ""
|
|
self.category = category
|
|
self.options = options
|
|
self.min_value = min_value
|
|
self.max_value = max_value
|
|
self.step = step
|
|
self.visible = visible
|
|
self.updated_at = None
|
|
|
|
|
|
class TestGetBoolSetting:
|
|
"""Tests for get_bool_setting method."""
|
|
|
|
def test_get_bool_setting_true_string(self):
|
|
"""get_bool_setting converts 'true' string to True."""
|
|
from local_deep_research.settings.manager import (
|
|
SettingsManager,
|
|
)
|
|
|
|
mock_session = Mock()
|
|
mock_query = Mock()
|
|
mock_setting = MockSetting(
|
|
key="app.debug", value="true", ui_element="text"
|
|
)
|
|
mock_query.filter.return_value.all.return_value = [mock_setting]
|
|
mock_session.query.return_value = mock_query
|
|
|
|
manager = SettingsManager(db_session=mock_session)
|
|
result = manager.get_bool_setting("app.debug", check_env=False)
|
|
|
|
assert result is True
|
|
|
|
def test_get_bool_setting_false_string(self):
|
|
"""get_bool_setting converts 'false' string to False."""
|
|
from local_deep_research.settings.manager import (
|
|
SettingsManager,
|
|
)
|
|
|
|
mock_session = Mock()
|
|
mock_query = Mock()
|
|
mock_setting = MockSetting(
|
|
key="app.debug", value="false", ui_element="text"
|
|
)
|
|
mock_query.filter.return_value.all.return_value = [mock_setting]
|
|
mock_session.query.return_value = mock_query
|
|
|
|
manager = SettingsManager(db_session=mock_session)
|
|
result = manager.get_bool_setting("app.debug", check_env=False)
|
|
|
|
assert result is False
|
|
|
|
def test_get_bool_setting_1_string(self):
|
|
"""get_bool_setting converts '1' string to True."""
|
|
from local_deep_research.settings.manager import (
|
|
SettingsManager,
|
|
)
|
|
|
|
mock_session = Mock()
|
|
mock_query = Mock()
|
|
mock_setting = MockSetting(
|
|
key="app.debug", value="1", ui_element="text"
|
|
)
|
|
mock_query.filter.return_value.all.return_value = [mock_setting]
|
|
mock_session.query.return_value = mock_query
|
|
|
|
manager = SettingsManager(db_session=mock_session)
|
|
result = manager.get_bool_setting("app.debug", check_env=False)
|
|
|
|
assert result is True
|
|
|
|
def test_get_bool_setting_0_string(self):
|
|
"""get_bool_setting converts '0' string to False."""
|
|
from local_deep_research.settings.manager import (
|
|
SettingsManager,
|
|
)
|
|
|
|
mock_session = Mock()
|
|
mock_query = Mock()
|
|
mock_setting = MockSetting(
|
|
key="app.debug", value="0", ui_element="text"
|
|
)
|
|
mock_query.filter.return_value.all.return_value = [mock_setting]
|
|
mock_session.query.return_value = mock_query
|
|
|
|
manager = SettingsManager(db_session=mock_session)
|
|
result = manager.get_bool_setting("app.debug", check_env=False)
|
|
|
|
assert result is False
|
|
|
|
def test_get_bool_setting_boolean_true(self):
|
|
"""get_bool_setting handles actual boolean True."""
|
|
from local_deep_research.settings.manager import (
|
|
SettingsManager,
|
|
)
|
|
|
|
mock_session = Mock()
|
|
mock_query = Mock()
|
|
mock_setting = MockSetting(
|
|
key="app.debug", value=True, ui_element="checkbox"
|
|
)
|
|
mock_query.filter.return_value.all.return_value = [mock_setting]
|
|
mock_session.query.return_value = mock_query
|
|
|
|
manager = SettingsManager(db_session=mock_session)
|
|
result = manager.get_bool_setting("app.debug", check_env=False)
|
|
|
|
assert result is True
|
|
|
|
def test_get_bool_setting_returns_default(self):
|
|
"""get_bool_setting returns default when setting not found."""
|
|
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)
|
|
result = manager.get_bool_setting("app.nonexistent", default=True)
|
|
|
|
assert result is True
|
|
|
|
def test_get_bool_setting_yes_string(self):
|
|
"""get_bool_setting converts 'yes' string to True."""
|
|
from local_deep_research.settings.manager import (
|
|
SettingsManager,
|
|
)
|
|
|
|
mock_session = Mock()
|
|
mock_query = Mock()
|
|
mock_setting = MockSetting(
|
|
key="app.enabled", value="yes", ui_element="text"
|
|
)
|
|
mock_query.filter.return_value.all.return_value = [mock_setting]
|
|
mock_session.query.return_value = mock_query
|
|
|
|
manager = SettingsManager(db_session=mock_session)
|
|
result = manager.get_bool_setting("app.enabled", check_env=False)
|
|
|
|
assert result is True
|
|
|
|
|
|
class TestUIElementTypeConversion:
|
|
"""Tests for UI element type conversion."""
|
|
|
|
def test_checkbox_returns_bool(self):
|
|
"""Checkbox ui_element returns boolean."""
|
|
from local_deep_research.settings.manager import (
|
|
SettingsManager,
|
|
)
|
|
|
|
mock_session = Mock()
|
|
mock_query = Mock()
|
|
mock_setting = MockSetting(
|
|
key="app.debug", value=True, ui_element="checkbox"
|
|
)
|
|
mock_query.filter.return_value.all.return_value = [mock_setting]
|
|
mock_session.query.return_value = mock_query
|
|
|
|
manager = SettingsManager(db_session=mock_session)
|
|
result = manager.get_setting("app.debug", check_env=False)
|
|
|
|
assert isinstance(result, bool)
|
|
|
|
def test_number_returns_float(self):
|
|
"""Number ui_element returns float."""
|
|
from local_deep_research.settings.manager import (
|
|
SettingsManager,
|
|
)
|
|
|
|
mock_session = Mock()
|
|
mock_query = Mock()
|
|
mock_setting = MockSetting(
|
|
key="llm.temperature", value=0.7, ui_element="number"
|
|
)
|
|
mock_query.filter.return_value.all.return_value = [mock_setting]
|
|
mock_session.query.return_value = mock_query
|
|
|
|
manager = SettingsManager(db_session=mock_session)
|
|
result = manager.get_setting("llm.temperature", check_env=False)
|
|
|
|
assert isinstance(result, float)
|
|
|
|
def test_range_returns_float(self):
|
|
"""Range ui_element returns float."""
|
|
from local_deep_research.settings.manager import (
|
|
SettingsManager,
|
|
)
|
|
|
|
mock_session = Mock()
|
|
mock_query = Mock()
|
|
mock_setting = MockSetting(
|
|
key="llm.top_p", value=0.9, ui_element="range"
|
|
)
|
|
mock_query.filter.return_value.all.return_value = [mock_setting]
|
|
mock_session.query.return_value = mock_query
|
|
|
|
manager = SettingsManager(db_session=mock_session)
|
|
result = manager.get_setting("llm.top_p", check_env=False)
|
|
|
|
assert isinstance(result, float)
|
|
|
|
def test_text_returns_string(self):
|
|
"""Text ui_element returns string."""
|
|
from local_deep_research.settings.manager import (
|
|
SettingsManager,
|
|
)
|
|
|
|
mock_session = Mock()
|
|
mock_query = Mock()
|
|
mock_setting = MockSetting(
|
|
key="llm.model", value="gpt-4", ui_element="text"
|
|
)
|
|
mock_query.filter.return_value.all.return_value = [mock_setting]
|
|
mock_session.query.return_value = mock_query
|
|
|
|
manager = SettingsManager(db_session=mock_session)
|
|
result = manager.get_setting("llm.model", check_env=False)
|
|
|
|
assert isinstance(result, str)
|
|
|
|
def test_password_returns_string(self):
|
|
"""Password ui_element returns string."""
|
|
from local_deep_research.settings.manager import (
|
|
SettingsManager,
|
|
)
|
|
|
|
mock_session = Mock()
|
|
mock_query = Mock()
|
|
mock_setting = MockSetting(
|
|
key="llm.api_key", value="secret123", ui_element="password"
|
|
)
|
|
mock_query.filter.return_value.all.return_value = [mock_setting]
|
|
mock_session.query.return_value = mock_query
|
|
|
|
manager = SettingsManager(db_session=mock_session)
|
|
result = manager.get_setting("llm.api_key", check_env=False)
|
|
|
|
assert isinstance(result, str)
|
|
|
|
def test_select_returns_string(self):
|
|
"""Select ui_element returns string."""
|
|
from local_deep_research.settings.manager import (
|
|
SettingsManager,
|
|
)
|
|
|
|
mock_session = Mock()
|
|
mock_query = Mock()
|
|
mock_setting = MockSetting(
|
|
key="app.theme", value="dark", ui_element="select"
|
|
)
|
|
mock_query.filter.return_value.all.return_value = [mock_setting]
|
|
mock_session.query.return_value = mock_query
|
|
|
|
manager = SettingsManager(db_session=mock_session)
|
|
result = manager.get_setting("app.theme", check_env=False)
|
|
|
|
assert isinstance(result, str)
|
|
|
|
def test_json_returns_as_is(self):
|
|
"""JSON ui_element returns value as-is."""
|
|
from local_deep_research.settings.manager import (
|
|
SettingsManager,
|
|
)
|
|
|
|
mock_session = Mock()
|
|
mock_query = Mock()
|
|
json_value = {"key": "value", "nested": {"data": 123}}
|
|
mock_setting = MockSetting(
|
|
key="app.config", value=json_value, ui_element="json"
|
|
)
|
|
mock_query.filter.return_value.all.return_value = [mock_setting]
|
|
mock_session.query.return_value = mock_query
|
|
|
|
manager = SettingsManager(db_session=mock_session)
|
|
result = manager.get_setting("app.config", check_env=False)
|
|
|
|
assert result == json_value
|
|
|
|
def test_unknown_ui_element_returns_default(self):
|
|
"""Unknown ui_element returns default value."""
|
|
from local_deep_research.settings.manager import (
|
|
SettingsManager,
|
|
)
|
|
|
|
mock_session = Mock()
|
|
mock_query = Mock()
|
|
mock_setting = MockSetting(
|
|
key="app.unknown", value="test", ui_element="unknown_type"
|
|
)
|
|
mock_query.filter.return_value.all.return_value = [mock_setting]
|
|
mock_session.query.return_value = mock_query
|
|
|
|
manager = SettingsManager(db_session=mock_session)
|
|
result = manager.get_setting(
|
|
"app.unknown", default="default_val", check_env=False
|
|
)
|
|
|
|
assert result == "default_val"
|
|
|
|
|
|
class TestEnvironmentVariableEdgeCases:
|
|
"""Tests for environment variable handling edge cases."""
|
|
|
|
def test_env_override_invalid_number(self):
|
|
"""Invalid number in env var falls back to DB value."""
|
|
from local_deep_research.settings.manager import (
|
|
SettingsManager,
|
|
)
|
|
|
|
mock_session = Mock()
|
|
mock_query = Mock()
|
|
mock_setting = MockSetting(
|
|
key="llm.temperature", value=0.7, ui_element="number"
|
|
)
|
|
mock_query.filter.return_value.all.return_value = [mock_setting]
|
|
mock_session.query.return_value = mock_query
|
|
|
|
os.environ["LDR_LLM_TEMPERATURE"] = "not_a_number"
|
|
try:
|
|
manager = SettingsManager(db_session=mock_session)
|
|
result = manager.get_setting("llm.temperature", check_env=True)
|
|
|
|
# Should fall back to DB value
|
|
assert result == 0.7
|
|
finally:
|
|
del os.environ["LDR_LLM_TEMPERATURE"]
|
|
|
|
def test_env_override_empty_string(self):
|
|
"""Empty string env var is treated as unset (Docker/Unraid templates create blank entries)."""
|
|
from local_deep_research.settings.manager import (
|
|
SettingsManager,
|
|
)
|
|
|
|
mock_session = Mock()
|
|
mock_query = Mock()
|
|
mock_setting = MockSetting(
|
|
key="llm.model", value="gpt-4", ui_element="text"
|
|
)
|
|
mock_query.filter.return_value.all.return_value = [mock_setting]
|
|
mock_session.query.return_value = mock_query
|
|
|
|
os.environ["LDR_LLM_MODEL"] = ""
|
|
try:
|
|
manager = SettingsManager(db_session=mock_session)
|
|
result = manager.get_setting("llm.model", check_env=True)
|
|
|
|
# Empty env var should NOT override the DB value
|
|
assert result == "gpt-4"
|
|
finally:
|
|
del os.environ["LDR_LLM_MODEL"]
|
|
|
|
def test_env_override_empty_string_no_db_value(self):
|
|
"""Empty env var with no DB value falls through to default.
|
|
|
|
This is the first-launch Unraid scenario: env vars are empty strings,
|
|
no DB row exists yet, so the default should be returned.
|
|
"""
|
|
from local_deep_research.settings.manager import (
|
|
SettingsManager,
|
|
)
|
|
|
|
mock_session = Mock()
|
|
mock_query = Mock()
|
|
# No DB rows match this key
|
|
mock_query.filter.return_value.all.return_value = []
|
|
mock_session.query.return_value = mock_query
|
|
|
|
os.environ["LDR_LLM_PROVIDER"] = ""
|
|
try:
|
|
manager = SettingsManager(db_session=mock_session)
|
|
result = manager.get_setting(
|
|
"llm.provider", default="openai", check_env=True
|
|
)
|
|
assert result == "openai"
|
|
finally:
|
|
del os.environ["LDR_LLM_PROVIDER"]
|
|
|
|
def test_env_override_empty_string_checkbox(self):
|
|
"""Empty env var for checkbox setting falls through to DB value."""
|
|
from local_deep_research.settings.manager import (
|
|
SettingsManager,
|
|
)
|
|
|
|
mock_session = Mock()
|
|
mock_query = Mock()
|
|
mock_setting = MockSetting(
|
|
key="app.debug", value=True, ui_element="checkbox"
|
|
)
|
|
mock_query.filter.return_value.all.return_value = [mock_setting]
|
|
mock_session.query.return_value = mock_query
|
|
|
|
os.environ["LDR_APP_DEBUG"] = ""
|
|
try:
|
|
manager = SettingsManager(db_session=mock_session)
|
|
result = manager.get_setting("app.debug", check_env=True)
|
|
assert result is True
|
|
finally:
|
|
del os.environ["LDR_APP_DEBUG"]
|
|
|
|
def test_env_override_empty_string_number(self):
|
|
"""Empty env var for number setting falls through to DB value."""
|
|
from local_deep_research.settings.manager import (
|
|
SettingsManager,
|
|
)
|
|
|
|
mock_session = Mock()
|
|
mock_query = Mock()
|
|
mock_setting = MockSetting(
|
|
key="llm.temperature", value=0.7, ui_element="number"
|
|
)
|
|
mock_query.filter.return_value.all.return_value = [mock_setting]
|
|
mock_session.query.return_value = mock_query
|
|
|
|
os.environ["LDR_LLM_TEMPERATURE"] = ""
|
|
try:
|
|
manager = SettingsManager(db_session=mock_session)
|
|
result = manager.get_setting("llm.temperature", check_env=True)
|
|
assert result == 0.7
|
|
finally:
|
|
del os.environ["LDR_LLM_TEMPERATURE"]
|
|
|
|
def test_env_override_with_nested_key(self):
|
|
"""Nested key with multiple dots converts correctly."""
|
|
from local_deep_research.settings.manager import (
|
|
check_env_setting,
|
|
)
|
|
|
|
os.environ["LDR_SEARCH_TOOL_MAX_RESULTS"] = "50"
|
|
try:
|
|
result = check_env_setting("search.tool.max_results")
|
|
assert result == "50"
|
|
finally:
|
|
del os.environ["LDR_SEARCH_TOOL_MAX_RESULTS"]
|
|
|
|
|
|
class TestNestedSettingsPaths:
|
|
"""Tests for nested settings path handling."""
|
|
|
|
def test_get_setting_returns_nested_dict(self):
|
|
"""Getting higher-level key returns nested dict."""
|
|
from local_deep_research.settings.manager import (
|
|
SettingsManager,
|
|
)
|
|
|
|
mock_session = Mock()
|
|
mock_query = Mock()
|
|
mock_settings = [
|
|
MockSetting(key="llm.model", value="gpt-4", ui_element="text"),
|
|
MockSetting(key="llm.temperature", value=0.7, ui_element="number"),
|
|
]
|
|
mock_query.filter.return_value.all.return_value = mock_settings
|
|
mock_session.query.return_value = mock_query
|
|
|
|
manager = SettingsManager(db_session=mock_session)
|
|
result = manager.get_setting("llm", check_env=False)
|
|
|
|
assert isinstance(result, dict)
|
|
assert "model" in result
|
|
assert "temperature" in result
|
|
|
|
def test_get_setting_exact_match_single(self):
|
|
"""Getting exact key returns single value."""
|
|
from local_deep_research.settings.manager import (
|
|
SettingsManager,
|
|
)
|
|
|
|
mock_session = Mock()
|
|
mock_query = Mock()
|
|
mock_setting = MockSetting(
|
|
key="llm.model", value="gpt-4", ui_element="text"
|
|
)
|
|
mock_query.filter.return_value.all.return_value = [mock_setting]
|
|
mock_session.query.return_value = mock_query
|
|
|
|
manager = SettingsManager(db_session=mock_session)
|
|
result = manager.get_setting("llm.model", check_env=False)
|
|
|
|
assert result == "gpt-4"
|
|
|
|
|
|
class TestImportExportSettings:
|
|
"""Tests for import/export functionality."""
|
|
|
|
def test_import_settings_basic(self):
|
|
"""import_settings loads settings from dict."""
|
|
from local_deep_research.settings.manager import (
|
|
SettingsManager,
|
|
)
|
|
|
|
mock_session = Mock()
|
|
mock_query = Mock()
|
|
mock_query.filter.return_value.delete.return_value = 1
|
|
mock_session.query.return_value = mock_query
|
|
|
|
manager = SettingsManager(db_session=mock_session)
|
|
|
|
settings_data = {
|
|
"app.debug": {
|
|
"value": True,
|
|
"type": "APP",
|
|
"name": "Debug",
|
|
"ui_element": "checkbox",
|
|
},
|
|
}
|
|
|
|
with patch.object(manager, "_emit_settings_changed"):
|
|
manager.import_settings(settings_data)
|
|
|
|
mock_session.add.assert_called()
|
|
mock_session.commit.assert_called()
|
|
|
|
def test_import_settings_no_overwrite(self):
|
|
"""import_settings preserves existing values when overwrite=False."""
|
|
from local_deep_research.settings.manager import (
|
|
SettingsManager,
|
|
)
|
|
|
|
mock_session = Mock()
|
|
mock_query = Mock()
|
|
mock_query.filter.return_value.delete.return_value = 0
|
|
mock_session.query.return_value = mock_query
|
|
|
|
manager = SettingsManager(db_session=mock_session)
|
|
manager.get_setting = Mock(return_value="existing_value")
|
|
|
|
settings_data = {
|
|
"app.key": {
|
|
"value": "new_value",
|
|
"type": "APP",
|
|
"name": "Key",
|
|
"ui_element": "text",
|
|
},
|
|
}
|
|
|
|
with patch.object(manager, "_emit_settings_changed"):
|
|
manager.import_settings(settings_data, overwrite=False)
|
|
|
|
# Check that existing value was preserved
|
|
added_setting = mock_session.add.call_args[0][0]
|
|
assert added_setting.value == "existing_value"
|
|
|
|
def test_import_settings_delete_extra(self):
|
|
"""import_settings deletes extra settings when delete_extra=True."""
|
|
from local_deep_research.settings.manager import (
|
|
SettingsManager,
|
|
)
|
|
|
|
mock_session = Mock()
|
|
mock_query = Mock()
|
|
mock_query.filter.return_value.delete.return_value = 1
|
|
mock_session.query.return_value = mock_query
|
|
|
|
manager = SettingsManager(db_session=mock_session)
|
|
|
|
# Mock get_all_settings to return extra setting
|
|
manager.get_all_settings = Mock(
|
|
return_value={
|
|
"app.keep": {"value": True},
|
|
"app.extra": {"value": False},
|
|
}
|
|
)
|
|
manager.delete_setting = Mock(return_value=True)
|
|
|
|
settings_data = {
|
|
"app.keep": {
|
|
"value": True,
|
|
"type": "APP",
|
|
"name": "Keep",
|
|
"ui_element": "checkbox",
|
|
},
|
|
}
|
|
|
|
with patch.object(manager, "_emit_settings_changed"):
|
|
manager.import_settings(settings_data, delete_extra=True)
|
|
|
|
# Should have deleted the extra setting
|
|
manager.delete_setting.assert_any_call("app.extra", commit=False)
|
|
|
|
|
|
class TestLoadFromDefaultsFile:
|
|
"""Tests for load_from_defaults_file method."""
|
|
|
|
def test_load_from_defaults_file_calls_import(self):
|
|
"""load_from_defaults_file calls import_settings."""
|
|
from local_deep_research.settings.manager import (
|
|
SettingsManager,
|
|
)
|
|
|
|
mock_session = Mock()
|
|
manager = SettingsManager(db_session=mock_session)
|
|
|
|
# Mock import_settings and default_settings
|
|
manager.import_settings = Mock()
|
|
manager.__dict__.pop("default_settings", None)
|
|
|
|
with patch.object(
|
|
type(manager),
|
|
"default_settings",
|
|
new_callable=lambda: property(
|
|
lambda self: {"app.test": {"value": True}}
|
|
),
|
|
):
|
|
manager.load_from_defaults_file()
|
|
|
|
manager.import_settings.assert_called_once()
|
|
|
|
|
|
class TestVersionChecking:
|
|
"""Tests for version checking functionality."""
|
|
|
|
def test_update_db_version(self):
|
|
"""update_db_version updates the version in DB."""
|
|
from local_deep_research.settings.manager import (
|
|
SettingsManager,
|
|
)
|
|
from local_deep_research.__version__ import __version__
|
|
|
|
mock_session = Mock()
|
|
mock_query = Mock()
|
|
mock_query.filter.return_value.delete.return_value = 1
|
|
mock_session.query.return_value = mock_query
|
|
|
|
manager = SettingsManager(db_session=mock_session)
|
|
manager.update_db_version()
|
|
|
|
mock_session.add.assert_called_once()
|
|
added_setting = mock_session.add.call_args[0][0]
|
|
assert added_setting.value == __version__
|
|
mock_session.commit.assert_called_once()
|
|
|
|
|
|
class TestWebSocketEmission:
|
|
"""Tests for WebSocket event emission."""
|
|
|
|
@staticmethod
|
|
def _request_ctx(username="alice"):
|
|
"""A Flask request context with ``username`` in the session, so
|
|
_emit_settings_changed can resolve the owning user to scope the emit."""
|
|
from flask import Flask, session
|
|
|
|
app = Flask(__name__)
|
|
app.secret_key = "test-secret"
|
|
ctx = app.test_request_context()
|
|
ctx.push()
|
|
if username is not None:
|
|
session["username"] = username
|
|
return ctx
|
|
|
|
def _patched_socket(self, mock_socket_instance):
|
|
# Give user_room a realistic return so room scoping can be asserted.
|
|
mock_socket_instance.user_room.side_effect = lambda u: f"user:{u}"
|
|
return patch.dict(
|
|
"sys.modules",
|
|
{
|
|
"local_deep_research.web.services.socket_service": MagicMock(
|
|
SocketIOService=Mock(return_value=mock_socket_instance)
|
|
)
|
|
},
|
|
)
|
|
|
|
@staticmethod
|
|
def _working_manager():
|
|
"""A SettingsManager whose get_setting("app.debug") resolves to a real
|
|
value, so the only thing that can suppress the emit is the scoping
|
|
guard under test — not an incidental failure reading the setting."""
|
|
from local_deep_research.settings.manager import SettingsManager
|
|
|
|
mock_session = Mock()
|
|
mock_query = Mock()
|
|
mock_query.filter.return_value.all.return_value = [
|
|
MockSetting(key="app.debug", value=True)
|
|
]
|
|
mock_session.query.return_value = mock_query
|
|
return SettingsManager(db_session=mock_session)
|
|
|
|
def test_emit_settings_changed_scoped_to_owning_user_room(self):
|
|
"""The event is emitted only to the changing user's per-user room —
|
|
never broadcast — so one user's setting values (incl. plaintext API
|
|
keys) never reach other connected clients."""
|
|
from local_deep_research.settings.manager import SettingsManager
|
|
|
|
mock_session = Mock()
|
|
mock_query = Mock()
|
|
mock_setting = MockSetting(key="app.debug", value=True)
|
|
mock_query.filter.return_value.all.return_value = [mock_setting]
|
|
mock_session.query.return_value = mock_query
|
|
|
|
mock_socket_instance = Mock()
|
|
manager = SettingsManager(db_session=mock_session)
|
|
|
|
ctx = self._request_ctx(username="alice")
|
|
try:
|
|
with self._patched_socket(mock_socket_instance):
|
|
manager._emit_settings_changed(["app.debug"])
|
|
finally:
|
|
ctx.pop()
|
|
|
|
mock_socket_instance.emit_socket_event.assert_called_once()
|
|
args, kwargs = mock_socket_instance.emit_socket_event.call_args
|
|
assert args[0] == "settings_changed"
|
|
assert "app.debug" in args[1]["changed_keys"]
|
|
# The targeting room must be the owning user's room.
|
|
assert kwargs.get("room") == "user:alice"
|
|
|
|
def test_emit_settings_changed_skipped_without_request_context(self):
|
|
"""A settings change outside any request (start-up defaults, background
|
|
workers, migrations) has no user tab to notify, so nothing is emitted
|
|
— crucially, it must not fall back to a cross-user broadcast."""
|
|
mock_socket_instance = Mock()
|
|
manager = self._working_manager()
|
|
|
|
# No request context pushed.
|
|
with self._patched_socket(mock_socket_instance):
|
|
manager._emit_settings_changed(["app.debug"])
|
|
|
|
mock_socket_instance.emit_socket_event.assert_not_called()
|
|
|
|
def test_emit_settings_changed_skipped_when_no_username(self):
|
|
"""A request without an authenticated username emits nothing."""
|
|
mock_socket_instance = Mock()
|
|
manager = self._working_manager()
|
|
|
|
ctx = self._request_ctx(username=None)
|
|
try:
|
|
with self._patched_socket(mock_socket_instance):
|
|
manager._emit_settings_changed(["app.debug"])
|
|
finally:
|
|
ctx.pop()
|
|
|
|
mock_socket_instance.emit_socket_event.assert_not_called()
|
|
|
|
def test_emit_settings_changed_handles_exception(self):
|
|
"""_emit_settings_changed handles exceptions gracefully."""
|
|
from local_deep_research.settings.manager import (
|
|
SettingsManager,
|
|
)
|
|
|
|
mock_session = Mock()
|
|
manager = SettingsManager(db_session=mock_session)
|
|
|
|
# Mock SocketIOService to raise ValueError
|
|
mock_socket_module = MagicMock()
|
|
mock_socket_module.SocketIOService.side_effect = ValueError(
|
|
"Not initialized"
|
|
)
|
|
|
|
with patch.dict(
|
|
"sys.modules",
|
|
{
|
|
"local_deep_research.web.services.socket_service": mock_socket_module
|
|
},
|
|
):
|
|
# Should not raise
|
|
manager._emit_settings_changed(["app.debug"])
|
|
|
|
|
|
class TestDefaultSettingsProperty:
|
|
"""Tests for default_settings property."""
|
|
|
|
@patch("local_deep_research.settings.manager.defaults")
|
|
def test_default_settings_loads_json_files(self, mock_defaults):
|
|
"""default_settings loads JSON files from defaults directory."""
|
|
from local_deep_research.settings.manager import (
|
|
SettingsManager,
|
|
)
|
|
import tempfile
|
|
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
# Create mock defaults path
|
|
from pathlib import Path
|
|
|
|
mock_defaults.__file__ = str(Path(tmpdir) / "__init__.py")
|
|
|
|
# Create a test JSON file
|
|
json_file = Path(tmpdir) / "test_settings.json"
|
|
json_file.write_text(
|
|
json.dumps(
|
|
{
|
|
"test.setting": {
|
|
"value": "test_value",
|
|
"type": "APP",
|
|
"name": "Test",
|
|
"ui_element": "text",
|
|
}
|
|
}
|
|
)
|
|
)
|
|
|
|
manager = SettingsManager()
|
|
settings = manager.default_settings
|
|
|
|
assert "test.setting" in settings
|
|
|
|
def test_default_settings_handles_invalid_json(self):
|
|
"""default_settings handles invalid JSON gracefully."""
|
|
from local_deep_research.settings.manager import (
|
|
SettingsManager,
|
|
)
|
|
|
|
manager = SettingsManager()
|
|
|
|
# Should not raise even if there are issues
|
|
settings = manager.default_settings
|
|
assert isinstance(settings, dict)
|
|
|
|
|
|
class TestSettingTypeDetermination:
|
|
"""Tests for automatic setting type determination."""
|
|
|
|
def test_set_setting_determines_llm_type(self):
|
|
"""set_setting creates LLM type for llm. keys."""
|
|
from local_deep_research.settings.manager import (
|
|
SettingsManager,
|
|
)
|
|
from local_deep_research.database.models import SettingType
|
|
|
|
mock_session = Mock()
|
|
mock_query = Mock()
|
|
mock_query.filter.return_value.first.return_value = None
|
|
mock_session.query.return_value = mock_query
|
|
|
|
manager = SettingsManager(db_session=mock_session)
|
|
|
|
with patch.object(manager, "_emit_settings_changed"):
|
|
manager.set_setting("llm.new_setting", "value")
|
|
|
|
added_setting = mock_session.add.call_args[0][0]
|
|
assert added_setting.type == SettingType.LLM
|
|
|
|
def test_set_setting_determines_search_type(self):
|
|
"""set_setting creates SEARCH type for search. keys."""
|
|
from local_deep_research.settings.manager import (
|
|
SettingsManager,
|
|
)
|
|
from local_deep_research.database.models import SettingType
|
|
|
|
mock_session = Mock()
|
|
mock_query = Mock()
|
|
mock_query.filter.return_value.first.return_value = None
|
|
mock_session.query.return_value = mock_query
|
|
|
|
manager = SettingsManager(db_session=mock_session)
|
|
|
|
with patch.object(manager, "_emit_settings_changed"):
|
|
manager.set_setting("search.new_setting", "value")
|
|
|
|
added_setting = mock_session.add.call_args[0][0]
|
|
assert added_setting.type == SettingType.SEARCH
|
|
|
|
def test_set_setting_determines_report_type(self):
|
|
"""set_setting creates REPORT type for report. keys."""
|
|
from local_deep_research.settings.manager import (
|
|
SettingsManager,
|
|
)
|
|
from local_deep_research.database.models import SettingType
|
|
|
|
mock_session = Mock()
|
|
mock_query = Mock()
|
|
mock_query.filter.return_value.first.return_value = None
|
|
mock_session.query.return_value = mock_query
|
|
|
|
manager = SettingsManager(db_session=mock_session)
|
|
|
|
with patch.object(manager, "_emit_settings_changed"):
|
|
manager.set_setting("report.new_setting", "value")
|
|
|
|
added_setting = mock_session.add.call_args[0][0]
|
|
assert added_setting.type == SettingType.REPORT
|
|
|
|
def test_set_setting_defaults_to_app_type(self):
|
|
"""set_setting creates APP type for other keys."""
|
|
from local_deep_research.settings.manager import (
|
|
SettingsManager,
|
|
)
|
|
from local_deep_research.database.models import SettingType
|
|
|
|
mock_session = Mock()
|
|
mock_query = Mock()
|
|
mock_query.filter.return_value.first.return_value = None
|
|
mock_session.query.return_value = mock_query
|
|
|
|
manager = SettingsManager(db_session=mock_session)
|
|
|
|
with patch.object(manager, "_emit_settings_changed"):
|
|
manager.set_setting("other.new_setting", "value")
|
|
|
|
added_setting = mock_session.add.call_args[0][0]
|
|
assert added_setting.type == SettingType.APP
|
|
|
|
|
|
class TestCreateOrUpdateSettingTypes:
|
|
"""Tests for create_or_update_setting type handling."""
|
|
|
|
def test_create_or_update_with_llm_setting_class(self):
|
|
"""create_or_update_setting handles LLMSetting class."""
|
|
from local_deep_research.settings.manager import (
|
|
SettingsManager,
|
|
)
|
|
|
|
mock_session = Mock()
|
|
mock_query = Mock()
|
|
mock_query.filter.return_value.first.return_value = None
|
|
mock_session.query.return_value = mock_query
|
|
|
|
manager = SettingsManager(db_session=mock_session)
|
|
|
|
with patch.object(manager, "_emit_settings_changed"):
|
|
result = manager.create_or_update_setting(
|
|
{
|
|
"key": "llm.model",
|
|
"value": "gpt-4",
|
|
"name": "Model",
|
|
"description": "LLM model",
|
|
}
|
|
)
|
|
|
|
assert result is not None
|
|
|
|
def test_create_or_update_with_search_setting_class(self):
|
|
"""create_or_update_setting handles SearchSetting class."""
|
|
from local_deep_research.settings.manager import (
|
|
SettingsManager,
|
|
)
|
|
|
|
mock_session = Mock()
|
|
mock_query = Mock()
|
|
mock_query.filter.return_value.first.return_value = None
|
|
mock_session.query.return_value = mock_query
|
|
|
|
manager = SettingsManager(db_session=mock_session)
|
|
|
|
with patch.object(manager, "_emit_settings_changed"):
|
|
result = manager.create_or_update_setting(
|
|
{
|
|
"key": "search.max_results",
|
|
"value": 10,
|
|
"name": "Max Results",
|
|
"description": "Maximum results",
|
|
}
|
|
)
|
|
|
|
assert result is not None
|
|
|
|
def test_create_or_update_with_non_editable_returns_none(self):
|
|
"""create_or_update_setting returns None for non-editable setting."""
|
|
from local_deep_research.settings.manager import (
|
|
SettingsManager,
|
|
)
|
|
|
|
mock_session = Mock()
|
|
mock_query = Mock()
|
|
existing_setting = MockSetting(
|
|
key="app.version", value="1.0", editable=False
|
|
)
|
|
mock_query.filter.return_value.first.return_value = existing_setting
|
|
mock_session.query.return_value = mock_query
|
|
|
|
manager = SettingsManager(db_session=mock_session)
|
|
|
|
result = manager.create_or_update_setting(
|
|
{"key": "app.version", "value": "2.0", "name": "Version"}
|
|
)
|
|
|
|
assert result is None
|
|
|
|
|
|
class TestEdgeCases:
|
|
"""Tests for edge cases and boundary conditions."""
|
|
|
|
def test_get_setting_without_db_uses_defaults(self):
|
|
"""get_setting uses defaults when no DB session."""
|
|
from local_deep_research.settings.manager import (
|
|
SettingsManager,
|
|
)
|
|
|
|
manager = SettingsManager() # No db_session
|
|
|
|
# Should use default_settings
|
|
result = manager.get_setting("app.nonexistent", default="fallback")
|
|
|
|
# Returns default since setting likely doesn't exist
|
|
assert result == "fallback"
|
|
|
|
def test_get_all_settings_marks_locked_non_editable(self):
|
|
"""get_all_settings marks settings non-editable when locked."""
|
|
from local_deep_research.settings.manager import (
|
|
SettingsManager,
|
|
)
|
|
|
|
mock_session = Mock()
|
|
mock_query = Mock()
|
|
mock_settings = [
|
|
MockSetting(key="app.debug", value=True, editable=True),
|
|
]
|
|
mock_query.all.return_value = mock_settings
|
|
mock_session.query.return_value = mock_query
|
|
|
|
manager = SettingsManager(db_session=mock_session)
|
|
manager._SettingsManager__settings_locked = True
|
|
|
|
result = manager.get_all_settings()
|
|
|
|
assert result["app.debug"]["editable"] is False
|
|
|
|
def test_db_error_returns_default(self):
|
|
"""Database error returns default value."""
|
|
from local_deep_research.settings.manager import (
|
|
SettingsManager,
|
|
)
|
|
from sqlalchemy.exc import SQLAlchemyError
|
|
|
|
mock_session = Mock()
|
|
# Let __init__'s _ensure_settings_initialized() succeed
|
|
mock_session.query.return_value.count.return_value = 1
|
|
|
|
manager = SettingsManager(db_session=mock_session)
|
|
|
|
# Now make query raise for get_setting
|
|
mock_session.query.side_effect = SQLAlchemyError("DB Error")
|
|
result = manager.get_setting("app.test", default="default")
|
|
|
|
assert result == "default"
|
|
|
|
def test_thread_safety_no_db_passes(self):
|
|
"""Thread safety check passes without DB session."""
|
|
from local_deep_research.settings.manager import (
|
|
SettingsManager,
|
|
)
|
|
|
|
manager = SettingsManager() # No db_session
|
|
|
|
# Should not raise even from different thread concept
|
|
manager._check_thread_safety()
|
|
|
|
def test_set_setting_rollback_on_error(self):
|
|
"""set_setting calls rollback on SQLAlchemy error."""
|
|
from local_deep_research.settings.manager import (
|
|
SettingsManager,
|
|
)
|
|
from sqlalchemy.exc import SQLAlchemyError
|
|
|
|
mock_session = Mock()
|
|
mock_session.commit.side_effect = SQLAlchemyError("Commit failed")
|
|
mock_query = Mock()
|
|
mock_query.filter.return_value.first.return_value = MockSetting(
|
|
key="app.test", value="old"
|
|
)
|
|
mock_session.query.return_value = mock_query
|
|
|
|
manager = SettingsManager(db_session=mock_session)
|
|
result = manager.set_setting("app.test", "new")
|
|
|
|
assert result is False
|
|
mock_session.rollback.assert_called_once()
|