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
431 lines
15 KiB
Python
431 lines
15 KiB
Python
import os
|
|
from typing import Any
|
|
|
|
import pytest
|
|
from sqlalchemy.exc import SQLAlchemyError
|
|
|
|
from local_deep_research.settings.manager import (
|
|
SettingsManager,
|
|
check_env_setting,
|
|
)
|
|
from local_deep_research.database.models import Setting, SettingType
|
|
|
|
|
|
def test_check_env_setting_exists(monkeypatch):
|
|
monkeypatch.setenv("LDR_APP_VERSION", "1.0.0")
|
|
assert check_env_setting("app.version") == "1.0.0"
|
|
|
|
|
|
def test_check_env_setting_not_exists():
|
|
# Ensure the environment variable is not set
|
|
if "LDR_APP_VERSION" in os.environ:
|
|
del os.environ["LDR_APP_VERSION"]
|
|
assert check_env_setting("app.version") is None
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"ui_element, setting_value, expected",
|
|
[
|
|
("text", "hello", "hello"),
|
|
("select", "option_a", "option_a"),
|
|
("password", "secret", "secret"),
|
|
("number", "3.14", 3.14),
|
|
("range", "3.14", 3.14),
|
|
("checkbox", "true", True),
|
|
("json", {"key": "value"}, {"key": "value"}),
|
|
],
|
|
)
|
|
def test_get_setting_from_db(
|
|
mocker, ui_element: str, setting_value: str, expected: Any
|
|
):
|
|
"""
|
|
Tests that we can successfully read settings from the DB.
|
|
|
|
Args:
|
|
mocker: The fixture to use for mocking.
|
|
ui_element: The value of the `ui_element` parameter, which controls the return type.
|
|
setting_value: The value to use for the setting in the DB.
|
|
expected: The expected typed value of the setting.
|
|
"""
|
|
# Arrange: Set up the mock database session and a sample setting
|
|
mock_db_session = mocker.MagicMock()
|
|
mock_setting = Setting(
|
|
key="test.setting", value=setting_value, ui_element=ui_element
|
|
)
|
|
mock_db_session.query.return_value.filter.return_value.all.return_value = [
|
|
mock_setting
|
|
]
|
|
|
|
# Act: Call the get_setting method with the test key
|
|
settings_manager = SettingsManager(db_session=mock_db_session)
|
|
result = settings_manager.get_setting("test.setting")
|
|
|
|
# Assert: Verify that the result matches the expected value
|
|
assert result == expected, f"Expected {expected}, but got {result}"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"ui_element, env_value, expected",
|
|
[
|
|
("text", "hello", "hello"),
|
|
("select", "option_a", "option_a"),
|
|
("password", "secret", "secret"),
|
|
("number", "3.14", 3.14),
|
|
("range", "3.14", 3.14),
|
|
("checkbox", "true", True),
|
|
],
|
|
)
|
|
def test_get_setting_from_env(
|
|
mocker, monkeypatch, ui_element: str, env_value: str, expected: Any
|
|
):
|
|
"""
|
|
Tests that we can successfully override DB settings with environment
|
|
variables.
|
|
|
|
Args:
|
|
mocker: The fixture to use for mocking.
|
|
ui_element: The value of the `ui_element` parameter, which controls the return type.
|
|
env_value: The value to use for the setting in the environment variable.
|
|
expected: The expected typed value of the setting.
|
|
"""
|
|
# Arrange: Set up the mock database session and ensure an environment variable is set
|
|
mock_db_session = mocker.MagicMock()
|
|
monkeypatch.setenv("LDR_TEST_SETTING", env_value)
|
|
# Ensure DB query returns an old version to verify environment variable is prioritized
|
|
mock_setting = Setting(
|
|
key="test.setting", value="db_value", ui_element=ui_element
|
|
)
|
|
mock_db_session.query.return_value.filter.return_value.all.return_value = [
|
|
mock_setting
|
|
]
|
|
|
|
# Act: Call the get_setting method with the test key
|
|
settings_manager = SettingsManager(db_session=mock_db_session)
|
|
result = settings_manager.get_setting("test.setting")
|
|
|
|
# Assert: Verify that the result matches the expected value
|
|
assert result == expected, f"Expected {expected}, but got {result}"
|
|
|
|
|
|
def test_get_setting_default(mocker):
|
|
mock_db_session = mocker.MagicMock()
|
|
# Ensure DB query returns nothing
|
|
mock_db_session.query.return_value.filter.return_value.all.return_value = []
|
|
|
|
settings_manager = SettingsManager(db_session=mock_db_session)
|
|
assert (
|
|
settings_manager.get_setting(
|
|
"non_existent_setting", default="default_value"
|
|
)
|
|
== "default_value"
|
|
)
|
|
|
|
|
|
def test_get_setting_invalid_type(mocker):
|
|
"""
|
|
Tests that when a setting's value cannot be converted to the type
|
|
specified by the `ui_element`, it will always return the default value.
|
|
|
|
Args:
|
|
mocker: The fixture to use for mocking.
|
|
|
|
"""
|
|
# Arrange: Set up a mock DB session and setting with an invalid type
|
|
mock_db_session = mocker.MagicMock()
|
|
mock_setting = Setting(
|
|
key="test.invalid_type", value="not_a_number", ui_element="number"
|
|
)
|
|
mock_db_session.query.return_value.filter.return_value.all.return_value = [
|
|
mock_setting
|
|
]
|
|
|
|
# Act: Call get_setting with a default value and an invalid type
|
|
settings_manager = SettingsManager(db_session=mock_db_session)
|
|
result = settings_manager.get_setting("test.invalid_type", default=10)
|
|
|
|
# Assert: Check that the default value is returned and not the invalid string
|
|
assert result == 10
|
|
|
|
|
|
# The mocker fixture is automatically available
|
|
def test_set_setting_update_existing(mocker):
|
|
mock_db_session = mocker.MagicMock()
|
|
mock_setting = Setting(key="app.version", value="1.0.0", editable=True)
|
|
mock_db_session.query.return_value.filter.return_value.first.return_value = mock_setting
|
|
mocker.patch(
|
|
"local_deep_research.settings.manager.func.now"
|
|
) # Patching the func.now call
|
|
|
|
settings_manager = SettingsManager(db_session=mock_db_session)
|
|
# Mock the _check_thread_safety method to avoid thread safety errors in tests
|
|
mocker.patch.object(
|
|
settings_manager, "_check_thread_safety", return_value=None
|
|
)
|
|
|
|
result = settings_manager.set_setting("app.version", "2.0.0")
|
|
|
|
assert result is True
|
|
assert mock_setting.value == "2.0.0"
|
|
mock_db_session.commit.assert_called_once()
|
|
mock_db_session.rollback.assert_not_called()
|
|
|
|
|
|
def test_set_setting_create_new(mocker):
|
|
mock_db_session = mocker.MagicMock()
|
|
mock_db_session.query.return_value.filter.return_value.first.return_value = None
|
|
mocker.patch(
|
|
"local_deep_research.settings.manager.func.now"
|
|
) # Patching the func.now call
|
|
|
|
settings_manager = SettingsManager(db_session=mock_db_session)
|
|
result = settings_manager.set_setting("new.setting", "new_value")
|
|
|
|
assert result is True
|
|
mock_db_session.add.assert_called_once()
|
|
new_setting = mock_db_session.add.call_args[0][0]
|
|
assert isinstance(new_setting, Setting)
|
|
assert new_setting.key == "new.setting"
|
|
assert new_setting.value == "new_value"
|
|
assert (
|
|
new_setting.type == SettingType.APP
|
|
) # Assuming 'app' type for this key
|
|
mock_db_session.commit.assert_called_once()
|
|
mock_db_session.rollback.assert_not_called()
|
|
|
|
|
|
def test_set_setting_db_error(mocker):
|
|
mock_db_session = mocker.MagicMock()
|
|
# Set the side_effect to an instance of SQLAlchemyError
|
|
mock_db_session.query.return_value.filter.return_value.first.side_effect = (
|
|
SQLAlchemyError("Simulated DB Error")
|
|
)
|
|
|
|
# Mock the logger to check if error is logged
|
|
mock_logger = mocker.patch("local_deep_research.settings.manager.logger")
|
|
|
|
settings_manager = SettingsManager(db_session=mock_db_session)
|
|
result = settings_manager.set_setting("app.version", "2.0.0")
|
|
|
|
# Assert that the method returned False
|
|
assert result is False
|
|
|
|
mock_db_session.rollback.assert_called_once()
|
|
mock_db_session.commit.assert_not_called()
|
|
mock_logger.exception.assert_called_once()
|
|
# mock_logger.error.assert_called_once_with("Error setting value for app.version: Simulated DB Error")
|
|
|
|
|
|
def test_app_get_setting_from_real_db(
|
|
setup_database_for_all_tests, monkeypatch
|
|
):
|
|
"""
|
|
Tests get_setting via settings_service, using the real file-based database.
|
|
`setup_database_for_all_tests` yields `SessionLocal`, which is a sessionmaker.
|
|
"""
|
|
SessionLocal = setup_database_for_all_tests # Get the SessionLocal class from the fixture
|
|
session = SessionLocal() # Create a new session instance for this test
|
|
|
|
try:
|
|
# Add a setting to the database using this session
|
|
# Changed key to be specific to this test
|
|
setting = Setting(
|
|
key="test.app.version.get", # Use a unique key for this test
|
|
value="1.0.0",
|
|
type=SettingType.APP,
|
|
name="Version",
|
|
description="App version",
|
|
ui_element="text",
|
|
visible=True,
|
|
editable=True,
|
|
)
|
|
session.add(setting)
|
|
session.commit()
|
|
|
|
monkeypatch.delenv(
|
|
"LDR_APP_VERSION", raising=False
|
|
) # Ensure no env override
|
|
|
|
# Create settings manager with the session and get the setting
|
|
settings_manager = SettingsManager(db_session=session)
|
|
value = settings_manager.get_setting("test.app.version.get")
|
|
assert value == "1.0.0"
|
|
finally:
|
|
session.close() # Close the session for this test
|
|
|
|
|
|
def test_get_all_settings_from_real_file_db(
|
|
setup_database_for_all_tests, monkeypatch
|
|
):
|
|
"""
|
|
Tests retrieving all settings from a real *file-based* database.
|
|
"""
|
|
# Create a new session for this test
|
|
session = (
|
|
setup_database_for_all_tests()
|
|
) # Call the sessionmaker to get a new session
|
|
|
|
try:
|
|
# Add some settings to the database
|
|
setting1 = Setting(
|
|
key="all.settings.app.version",
|
|
value="1.0.0",
|
|
type=SettingType.APP,
|
|
name="Version",
|
|
description="App version",
|
|
ui_element="text",
|
|
visible=True,
|
|
editable=True,
|
|
)
|
|
setting2 = Setting(
|
|
key="all.settings.llm.temperature",
|
|
value=0.7,
|
|
type=SettingType.LLM,
|
|
name="Temperature",
|
|
description="LLM temperature",
|
|
ui_element="number",
|
|
visible=True,
|
|
editable=True,
|
|
)
|
|
session.add_all([setting1, setting2])
|
|
session.commit() # Commit to save them to the database
|
|
|
|
# Ensure no conflicting environment variables are set for this test
|
|
monkeypatch.delenv("LDR_APP_VERSION", raising=False)
|
|
monkeypatch.delenv("LDR_LLM_TEMPERATURE", raising=False)
|
|
|
|
settings_manager = SettingsManager(
|
|
db_session=session
|
|
) # Pass the session instance
|
|
all_settings = settings_manager.get_all_settings()
|
|
|
|
# Assertions should check for the unique keys
|
|
assert "all.settings.app.version" in all_settings
|
|
assert all_settings["all.settings.app.version"]["value"] == "1.0.0"
|
|
assert all_settings["all.settings.app.version"]["type"] == "APP"
|
|
assert all_settings["all.settings.app.version"]["editable"] is True
|
|
assert all_settings["all.settings.app.version"]["name"] == "Version"
|
|
|
|
assert "all.settings.llm.temperature" in all_settings
|
|
assert all_settings["all.settings.llm.temperature"]["value"] == 0.7
|
|
assert all_settings["all.settings.llm.temperature"]["type"] == "LLM"
|
|
assert all_settings["all.settings.llm.temperature"]["editable"] is True
|
|
assert (
|
|
all_settings["all.settings.llm.temperature"]["name"]
|
|
== "Temperature"
|
|
)
|
|
finally:
|
|
# IMPORTANT: Close the session created within the test
|
|
session.close()
|
|
|
|
|
|
def test_get_all_settings_db_error(mock_db_session, mock_logger):
|
|
"""Tests handling a SQLAlchemyError when retrieving all settings.
|
|
|
|
After unification, get_all_settings loads defaults first, then overlays
|
|
DB settings. When the DB query fails the defaults are still returned
|
|
(graceful degradation) and the error is logged.
|
|
"""
|
|
# Configure the mock session to raise a SQLAlchemyError
|
|
mock_db_session.query(Setting).all.side_effect = SQLAlchemyError(
|
|
"Simulated DB Error"
|
|
)
|
|
|
|
settings_manager = SettingsManager(db_session=mock_db_session)
|
|
all_settings = settings_manager.get_all_settings()
|
|
|
|
# Defaults are still loaded even when the DB query fails
|
|
assert isinstance(all_settings, dict)
|
|
assert len(all_settings) > 0
|
|
mock_logger.exception.assert_called_once()
|
|
# You can check the log message content if needed
|
|
|
|
|
|
def test_create_or_update_setting_does_not_app_prefix_local_search_keys(
|
|
setup_database_for_all_tests,
|
|
):
|
|
"""Regression for #4208: keys outside the llm./search./report./app.
|
|
buckets (e.g. local_search_*) must be written verbatim. Before the
|
|
fix, create_or_update_setting wrapped the input dict in AppSetting,
|
|
whose Pydantic validator silently rewrote `local_search_embedding_model`
|
|
to `app.local_search_embedding_model` — readers never found the value,
|
|
breaking every save on the Embeddings page.
|
|
"""
|
|
session_local_class = setup_database_for_all_tests
|
|
session = session_local_class()
|
|
|
|
with session:
|
|
settings_manager = SettingsManager(db_session=session)
|
|
setting_dict = {
|
|
"key": "local_search_embedding_model",
|
|
"value": "all-mpnet-base-v2",
|
|
"name": "Local Search Embedding Model",
|
|
"description": "Test",
|
|
}
|
|
result = settings_manager.create_or_update_setting(setting_dict)
|
|
assert result is not None
|
|
# The key must be persisted as-is, not relocated to app.<key>
|
|
assert result.key == "local_search_embedding_model"
|
|
|
|
roundtrip = settings_manager.get_setting("local_search_embedding_model")
|
|
assert roundtrip == "all-mpnet-base-v2"
|
|
|
|
# And the buggy `app.local_search_*` row must not exist.
|
|
bad_row = (
|
|
session.query(Setting)
|
|
.filter(Setting.key == "app.local_search_embedding_model")
|
|
.first()
|
|
)
|
|
assert bad_row is None
|
|
|
|
|
|
def test_get_setting_with_substring_keys(setup_database_for_all_tests):
|
|
"""
|
|
Tests that we can get the correct value for a setting, even when its key
|
|
is a substring of the key for a different setting.
|
|
|
|
Args:
|
|
setup_database_for_all_tests: Fixture that sets up a real database
|
|
for testing. (This is necessary because the bug fix this is
|
|
testing depends on the actual behavior of the SQLAlchemy `filter()`
|
|
function.)
|
|
|
|
"""
|
|
# Arrange.
|
|
session_local_class = setup_database_for_all_tests
|
|
session = session_local_class()
|
|
|
|
with session:
|
|
# Add two settings with overlapping keys but different full keys
|
|
setting1 = Setting(
|
|
key="test.hello",
|
|
value="world",
|
|
ui_element="text",
|
|
type="APP",
|
|
name="Test Setting 1",
|
|
visible=True,
|
|
editable=True,
|
|
)
|
|
setting2 = Setting(
|
|
key="test.hello_world",
|
|
value="universe",
|
|
ui_element="text",
|
|
type="APP",
|
|
name="Test Setting 2",
|
|
visible=True,
|
|
editable=True,
|
|
)
|
|
session.add(setting1)
|
|
session.add(setting2)
|
|
session.commit()
|
|
|
|
settings_manager = SettingsManager(db_session=session)
|
|
|
|
# Act and assert.
|
|
# Test getting the "test.hello" setting
|
|
result1 = settings_manager.get_setting("test.hello")
|
|
assert result1 == "world"
|
|
|
|
# Test getting the "test.hello_world" setting.
|
|
result2 = settings_manager.get_setting("test.hello_world")
|
|
assert result2 == "universe"
|