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

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"