""" Tests for the SettingsManager class. Tests cover: - Initialization - Getting settings - Setting values - Thread safety checks - Default settings loading - Environment variable overrides """ import os import threading from unittest.mock import Mock, 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 TestSettingsManagerInit: """Tests for SettingsManager initialization.""" def test_init_stores_db_session(self): """SettingsManager stores the database session.""" from local_deep_research.settings.manager import ( SettingsManager, ) mock_session = Mock() manager = SettingsManager(db_session=mock_session) assert manager.db_session is mock_session def test_init_without_db_session(self): """SettingsManager can be initialized without db session.""" from local_deep_research.settings.manager import ( SettingsManager, ) manager = SettingsManager() assert manager.db_session is None def test_init_stores_thread_id(self): """SettingsManager stores creation thread ID.""" from local_deep_research.settings.manager import ( SettingsManager, ) manager = SettingsManager() assert manager._creation_thread_id == threading.get_ident() class TestCheckEnvSetting: """Tests for check_env_setting function.""" def test_check_env_setting_found(self): """check_env_setting returns value when env var is set.""" from local_deep_research.settings.manager import ( check_env_setting, ) # Set environment variable os.environ["LDR_APP_DEBUG"] = "true" try: result = check_env_setting("app.debug") assert result == "true" finally: del os.environ["LDR_APP_DEBUG"] def test_check_env_setting_not_found(self): """check_env_setting returns None when env var not set.""" from local_deep_research.settings.manager import ( check_env_setting, ) # Make sure env var is not set if "LDR_APP_NONEXISTENT" in os.environ: del os.environ["LDR_APP_NONEXISTENT"] result = check_env_setting("app.nonexistent") assert result is None def test_check_env_setting_converts_dots_to_underscores(self): """check_env_setting converts dots to underscores in key.""" from local_deep_research.settings.manager import ( check_env_setting, ) os.environ["LDR_LLM_MODEL_NAME"] = "test-model" try: result = check_env_setting("llm.model.name") assert result == "test-model" finally: del os.environ["LDR_LLM_MODEL_NAME"] class TestSettingsManagerGetSetting: """Tests for SettingsManager.get_setting method.""" def test_get_setting_from_db(self): """get_setting retrieves value from database.""" 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 result is True def test_get_setting_returns_default_when_not_found(self): """get_setting returns default when setting not in database.""" 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_setting("app.nonexistent", default="default_value") assert result == "default_value" def test_get_setting_with_env_override(self): """get_setting uses environment variable when available.""" from local_deep_research.settings.manager import ( SettingsManager, ) mock_session = Mock() mock_query = Mock() mock_setting = MockSetting( key="app.test_value", value="db_value", ui_element="text" ) mock_query.filter.return_value.all.return_value = [mock_setting] mock_session.query.return_value = mock_query os.environ["LDR_APP_TEST_VALUE"] = "env_value" try: manager = SettingsManager(db_session=mock_session) result = manager.get_setting("app.test_value", check_env=True) assert result == "env_value" finally: del os.environ["LDR_APP_TEST_VALUE"] def test_get_setting_number_type(self): """get_setting correctly converts number types.""" 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 result == 0.7 assert isinstance(result, float) class TestSettingsManagerSetSetting: """Tests for SettingsManager.set_setting method.""" def test_set_setting_updates_existing(self): """set_setting updates existing setting in database.""" from local_deep_research.settings.manager import ( SettingsManager, ) mock_session = Mock() mock_query = Mock() mock_setting = MockSetting(key="app.debug", value=False, editable=True) mock_query.filter.return_value.first.return_value = mock_setting mock_session.query.return_value = mock_query manager = SettingsManager(db_session=mock_session) with patch.object(manager, "_emit_settings_changed"): result = manager.set_setting("app.debug", True) assert result is True assert mock_setting.value is True mock_session.commit.assert_called_once() def test_set_setting_fails_without_db_session(self): """set_setting returns False when no db session.""" from local_deep_research.settings.manager import ( SettingsManager, ) manager = SettingsManager() result = manager.set_setting("app.debug", True) assert result is False def test_set_setting_fails_for_non_editable(self): """set_setting returns False for non-editable settings.""" from local_deep_research.settings.manager import ( SettingsManager, ) mock_session = Mock() mock_query = Mock() mock_setting = MockSetting( key="app.version", value="1.0", editable=False ) mock_query.filter.return_value.first.return_value = mock_setting mock_session.query.return_value = mock_query manager = SettingsManager(db_session=mock_session) result = manager.set_setting("app.version", "2.0") assert result is False def test_set_setting_creates_new(self): """set_setting creates new setting when not exists.""" 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.set_setting("app.new_setting", "value") assert result is True mock_session.add.assert_called_once() class TestSettingsManagerThreadSafety: """Tests for SettingsManager thread safety.""" def test_thread_safety_check_same_thread(self): """_check_thread_safety passes in same thread.""" from local_deep_research.settings.manager import ( SettingsManager, ) mock_session = Mock() manager = SettingsManager(db_session=mock_session) # Should not raise manager._check_thread_safety() def test_thread_safety_check_different_thread(self): """_check_thread_safety raises in different thread.""" from local_deep_research.settings.manager import ( SettingsManager, ) mock_session = Mock() manager = SettingsManager(db_session=mock_session) error_raised = [False] def run_in_thread(): try: manager._check_thread_safety() except RuntimeError as e: if "not thread-safe" in str(e): error_raised[0] = True thread = threading.Thread(target=run_in_thread) thread.start() thread.join() assert error_raised[0] is True class TestSettingsManagerGetAllSettings: """Tests for SettingsManager.get_all_settings method.""" def test_get_all_settings_returns_dict(self): """get_all_settings returns dictionary of all settings.""" from local_deep_research.settings.manager import ( SettingsManager, ) mock_session = Mock() mock_query = Mock() mock_settings = [ MockSetting(key="app.debug", value=True, ui_element="checkbox"), MockSetting(key="llm.model", value="gpt-4", ui_element="text"), ] mock_query.all.return_value = mock_settings mock_session.query.return_value = mock_query manager = SettingsManager(db_session=mock_session) result = manager.get_all_settings() assert "app.debug" in result assert "llm.model" in result assert result["app.debug"]["value"] is True assert result["llm.model"]["value"] == "gpt-4" class TestSettingsManagerDeleteSetting: """Tests for SettingsManager.delete_setting method.""" def test_delete_setting_success(self): """delete_setting removes setting from database.""" 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) result = manager.delete_setting("app.old_setting") assert result is True mock_session.commit.assert_called_once() def test_delete_setting_not_found(self): """delete_setting returns False when setting not found.""" 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) result = manager.delete_setting("app.nonexistent") assert result is False def test_delete_setting_fails_without_db(self): """delete_setting returns False without db session.""" from local_deep_research.settings.manager import ( SettingsManager, ) manager = SettingsManager() result = manager.delete_setting("app.setting") assert result is False class TestSettingsManagerLocking: """Tests for settings locking functionality.""" def test_settings_locked_property(self): """settings_locked returns lock status.""" from local_deep_research.settings.manager import ( SettingsManager, ) mock_session = Mock() mock_query = Mock() mock_setting = MockSetting( key="app.lock_settings", 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) # Access property - should query and cache assert manager.settings_locked is True def test_set_setting_blocked_when_locked(self): """set_setting returns False when settings are locked.""" from local_deep_research.settings.manager import ( SettingsManager, ) mock_session = Mock() manager = SettingsManager(db_session=mock_session) # Force settings to be locked manager._SettingsManager__settings_locked = True result = manager.set_setting("app.debug", True) assert result is False class TestSettingsManagerCreateOrUpdate: """Tests for SettingsManager.create_or_update_setting method.""" def test_create_setting_with_dict(self): """create_or_update_setting accepts dictionary input.""" 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": "app.new_setting", "value": "test_value", "name": "New Setting", "description": "A new setting", } ) assert result is not None mock_session.add.assert_called_once() def test_update_existing_setting(self): """create_or_update_setting updates existing setting.""" from local_deep_research.settings.manager import ( SettingsManager, ) mock_session = Mock() mock_query = Mock() mock_setting = MockSetting( key="app.existing", value="old_value", editable=True ) mock_query.filter.return_value.first.return_value = mock_setting 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": "app.existing", "value": "new_value", "name": "Existing Setting", "description": "Updated", } ) assert result is not None assert mock_setting.value == "new_value" def test_create_or_update_fails_without_db(self): """create_or_update_setting returns None without db session.""" from local_deep_research.settings.manager import ( SettingsManager, ) manager = SettingsManager() result = manager.create_or_update_setting( {"key": "app.test", "value": "value"} ) assert result is None class TestSettingsManagerVersionCheck: """Tests for version checking functionality.""" def test_db_version_matches_package_true(self): """db_version_matches_package returns True when versions match.""" from local_deep_research.settings.manager import ( SettingsManager, ) from local_deep_research.__version__ import __version__ mock_session = Mock() mock_query = Mock() mock_setting = MockSetting( key="app.version", value=__version__, 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.db_version_matches_package() assert result is True def test_db_version_matches_package_false(self): """db_version_matches_package returns False when versions differ.""" from local_deep_research.settings.manager import ( SettingsManager, ) mock_session = Mock() mock_query = Mock() mock_setting = MockSetting( key="app.version", value="0.0.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.db_version_matches_package() assert result is False class TestGetSettingsSnapshot: """ Tests for get_settings_snapshot() method. This method is required by rag_routes.py which needs both get_bool_setting() and get_settings_snapshot() methods on SettingsManager. """ def test_get_settings_snapshot_exists(self): """Verify get_settings_snapshot method exists on SettingsManager.""" from local_deep_research.settings.manager import ( SettingsManager, ) manager = SettingsManager() assert hasattr(manager, "get_settings_snapshot"), ( "SettingsManager must have get_settings_snapshot method. " "This is required by rag_routes.py for background thread operations." ) assert callable(manager.get_settings_snapshot) def test_get_settings_snapshot_returns_dict(self): """get_settings_snapshot returns a dictionary.""" 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) result = manager.get_settings_snapshot() assert isinstance(result, dict) def test_get_settings_snapshot_returns_flat_dict(self): """get_settings_snapshot returns flat key-value pairs, not nested metadata.""" from local_deep_research.settings.manager import ( SettingsManager, ) mock_session = Mock() mock_query = Mock() mock_settings = [ MockSetting(key="app.debug", value=True, ui_element="checkbox"), MockSetting(key="app.name", value="TestApp", ui_element="text"), MockSetting( key="search.max_results", value=50, ui_element="number" ), ] mock_query.all.return_value = mock_settings mock_session.query.return_value = mock_query manager = SettingsManager(db_session=mock_session) result = manager.get_settings_snapshot() # Should be flat key-value pairs assert "app.debug" in result assert "app.name" in result assert "search.max_results" in result # Values should be direct values, not wrapped in {"value": ...} assert result["app.debug"] is True assert result["app.name"] == "TestApp" assert result["search.max_results"] == 50 def test_get_settings_snapshot_excludes_metadata(self): """get_settings_snapshot excludes metadata fields like ui_element, editable.""" from local_deep_research.settings.manager import ( SettingsManager, ) mock_session = Mock() mock_query = Mock() mock_settings = [ MockSetting( key="test.setting", value="test_value", ui_element="text", editable=True, description="A test setting", ), ] mock_query.all.return_value = mock_settings mock_session.query.return_value = mock_query manager = SettingsManager(db_session=mock_session) result = manager.get_settings_snapshot() # The value should be the setting value, not a metadata dict assert result.get("test.setting") == "test_value" # Should not contain metadata fields at the top level assert "ui_element" not in result assert "editable" not in result assert "description" not in result def test_get_settings_snapshot_supports_dict_assignment(self): """get_settings_snapshot result can be modified with dict assignment.""" 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) result = manager.get_settings_snapshot() # This is how rag_routes.py uses the snapshot result["_username"] = "testuser" assert result["_username"] == "testuser" def test_get_settings_snapshot_preserves_value_types(self): """get_settings_snapshot preserves the type of setting values.""" from local_deep_research.settings.manager import ( SettingsManager, ) mock_session = Mock() mock_query = Mock() mock_settings = [ MockSetting(key="bool_setting", value=True, ui_element="checkbox"), MockSetting(key="int_setting", value=42, ui_element="number"), MockSetting(key="str_setting", value="hello", ui_element="text"), MockSetting(key="none_setting", value=None, ui_element="text"), ] mock_query.all.return_value = mock_settings mock_session.query.return_value = mock_query manager = SettingsManager(db_session=mock_session) result = manager.get_settings_snapshot() assert result["bool_setting"] is True assert result["int_setting"] == 42 assert result["str_setting"] == "hello" assert result["none_setting"] is None def test_get_settings_snapshot_empty_database(self): """get_settings_snapshot works with empty database.""" 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) result = manager.get_settings_snapshot() assert isinstance(result, dict) # May contain default settings from files, but should not raise class TestGetBoolSettingMethod: """ Tests for get_bool_setting() method. This method is critical for rag_routes.py which calls it at lines 133, 2185, and 2309 to get boolean settings. """ def test_get_bool_setting_exists(self): """Verify get_bool_setting method exists on SettingsManager.""" from local_deep_research.settings.manager import ( SettingsManager, ) manager = SettingsManager() assert hasattr(manager, "get_bool_setting"), ( "SettingsManager must have get_bool_setting method. " "This is required by rag_routes.py for boolean settings." ) assert callable(manager.get_bool_setting) def test_get_bool_setting_returns_bool_type(self): """get_bool_setting always returns a boolean type.""" from local_deep_research.settings.manager import ( SettingsManager, ) mock_session = Mock() mock_query = Mock() # Return empty list to simulate setting not found mock_query.filter.return_value.all.return_value = [] mock_session.query.return_value = mock_query manager = SettingsManager(db_session=mock_session) result_true = manager.get_bool_setting( "nonexistent", True, check_env=False ) result_false = manager.get_bool_setting( "nonexistent", False, check_env=False ) assert isinstance(result_true, bool) assert isinstance(result_false, bool) assert result_true is True assert result_false is False def test_get_bool_setting_string_true_values(self): """get_bool_setting converts truthy strings to True.""" from local_deep_research.settings.manager import ( SettingsManager, ) # Use ui_element="text" to avoid checkbox type conversion # which would use Python's bool() on the raw string truthy_values = ["true", "True", "TRUE", "1", "yes", "on", "enabled"] for value in truthy_values: mock_session = Mock() mock_query = Mock() mock_setting = MockSetting( key="test.bool", value=value, 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( "test.bool", False, check_env=False ) assert result is True, f"Expected True for value '{value}'" def test_get_bool_setting_string_false_values(self): """get_bool_setting converts falsy strings to False.""" from local_deep_research.settings.manager import ( SettingsManager, ) # Use ui_element="text" to avoid checkbox type conversion # The to_bool() function treats strings not in truthy list as False falsy_values = ["false", "False", "FALSE", "0", "no", "off"] for value in falsy_values: mock_session = Mock() mock_query = Mock() mock_setting = MockSetting( key="test.bool", value=value, 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( "test.bool", True, check_env=False ) assert result is False, f"Expected False for value '{value}'" def test_get_bool_setting_boolean_passthrough(self): """get_bool_setting passes through actual boolean values.""" from local_deep_research.settings.manager import ( SettingsManager, ) for bool_value in [True, False]: mock_session = Mock() mock_query = Mock() mock_setting = MockSetting( key="test.bool", value=bool_value, 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( "test.bool", not bool_value, check_env=False ) assert result is bool_value def test_get_bool_setting_signature(self): """get_bool_setting has correct signature: (key, default, check_env).""" from local_deep_research.settings.manager import ( SettingsManager, ) import inspect manager = SettingsManager() sig = inspect.signature(manager.get_bool_setting) params = list(sig.parameters.keys()) assert "key" in params assert "default" in params assert "check_env" in params class TestSettingsManagerApiCompatibility: """ Tests to ensure settings.manager.SettingsManager has all methods required by code that was previously using separate implementations. These tests prevent regression of issue #1877. """ def test_has_all_required_methods_for_rag_routes(self): """SettingsManager has all methods required by rag_routes.py.""" from local_deep_research.settings.manager import ( SettingsManager, ) manager = SettingsManager() # Methods called in rag_routes.py required_methods = [ "get_setting", "get_bool_setting", "get_settings_snapshot", ] for method in required_methods: assert hasattr(manager, method), ( f"SettingsManager missing required method: {method}" ) assert callable(getattr(manager, method)), ( f"SettingsManager.{method} must be callable" ) def test_get_setting_and_get_bool_setting_consistency(self): """get_bool_setting should convert get_setting result to bool.""" from local_deep_research.settings.manager import ( SettingsManager, ) mock_session = Mock() mock_query = Mock() mock_setting = MockSetting( key="test.bool", value="true", ui_element="text" ) # Use correct mock pattern - filter.return_value.all.return_value mock_query.filter.return_value.all.return_value = [mock_setting] mock_session.query.return_value = mock_query manager = SettingsManager(db_session=mock_session) # get_setting returns raw value raw = manager.get_setting("test.bool", check_env=False) # get_bool_setting returns converted boolean converted = manager.get_bool_setting( "test.bool", False, check_env=False ) assert raw == "true" assert converted is True class TestGetBoolSettingEdgeCases: """ Edge case tests for get_bool_setting() method. These tests cover whitespace handling, case sensitivity, None values, and other edge cases. """ def test_get_bool_setting_whitespace_handling(self): """get_bool_setting handles values with leading/trailing whitespace.""" from local_deep_research.settings.manager import ( SettingsManager, ) # Test whitespace around truthy values whitespace_truthy = [" true ", "\ttrue\n", " 1 ", " yes "] for value in whitespace_truthy: mock_session = Mock() mock_query = Mock() mock_setting = MockSetting( key="test.bool", value=value, 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) # Note: to_bool uses .lower() which doesn't strip whitespace # This test documents actual behavior result = manager.get_bool_setting( "test.bool", False, check_env=False ) # Whitespace values may not match truthy list exactly assert isinstance(result, bool) def test_get_bool_setting_mixed_case(self): """get_bool_setting handles mixed case values.""" from local_deep_research.settings.manager import ( SettingsManager, ) mixed_case_values = [ "TrUe", "TRUE", "True", "tRuE", "YES", "Yes", "yEs", ] for value in mixed_case_values: mock_session = Mock() mock_query = Mock() mock_setting = MockSetting( key="test.bool", value=value, 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( "test.bool", False, check_env=False ) assert result is True, ( f"Expected True for mixed case value '{value}'" ) def test_get_bool_setting_none_value_behavior(self): """get_bool_setting handles None value from database. When a setting exists with value=None, get_typed_setting_value returns the default value (since value is None). So get_bool_setting returns to_bool(default, default) = default. """ from local_deep_research.settings.manager import ( SettingsManager, ) mock_session = Mock() mock_query = Mock() mock_setting = MockSetting( key="test.bool", value=None, 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) # When value is None, get_typed_setting_value returns the default # to_bool(True, True) = True result = manager.get_bool_setting("test.bool", True, check_env=False) assert result is True # Returns the default when DB value is None def test_get_bool_setting_empty_string_is_false(self): """get_bool_setting treats empty string as False.""" from local_deep_research.settings.manager import ( SettingsManager, ) mock_session = Mock() mock_query = Mock() mock_setting = MockSetting(key="test.bool", value="", 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("test.bool", True, check_env=False) # Empty string should be falsy assert result is False def test_get_bool_setting_numeric_zero_is_false(self): """get_bool_setting treats numeric 0 as False.""" from local_deep_research.settings.manager import ( SettingsManager, ) mock_session = Mock() mock_query = Mock() mock_setting = MockSetting( key="test.bool", value=0, 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_bool_setting("test.bool", True, check_env=False) assert result is False def test_get_bool_setting_numeric_nonzero_is_true(self): """get_bool_setting treats non-zero numbers as True.""" from local_deep_research.settings.manager import ( SettingsManager, ) for num_value in [1, 42, -1, 0.5, 100]: mock_session = Mock() mock_query = Mock() mock_setting = MockSetting( key="test.bool", value=num_value, 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_bool_setting( "test.bool", False, check_env=False ) assert result is True, ( f"Expected True for numeric value {num_value}" ) class TestGetBoolSettingEnvOverride: """ Tests for get_bool_setting() environment variable handling. These tests verify that environment variables properly override database settings when check_env=True. """ def test_get_bool_setting_env_override_true(self): """get_bool_setting uses env var when check_env=True.""" from local_deep_research.settings.manager import ( SettingsManager, ) mock_session = Mock() mock_query = Mock() # DB has False mock_setting = MockSetting( key="app.test_flag", value=False, ui_element="checkbox" ) mock_query.filter.return_value.all.return_value = [mock_setting] mock_session.query.return_value = mock_query # Env has "true" os.environ["LDR_APP_TEST_FLAG"] = "true" try: manager = SettingsManager(db_session=mock_session) result = manager.get_bool_setting( "app.test_flag", False, check_env=True ) # Env should override DB assert result is True finally: del os.environ["LDR_APP_TEST_FLAG"] def test_get_bool_setting_env_override_false_checkbox_behavior(self): """Document behavior: env var 'false' with checkbox ui_element. The unified SettingsManager uses parse_boolean() for checkbox type conversion, which correctly handles "false" -> False (unlike Python's bool("false") = True). """ from local_deep_research.settings.manager import ( SettingsManager, ) mock_session = Mock() mock_query = Mock() mock_setting = MockSetting( key="app.test_flag", value=True, ui_element="checkbox" ) mock_query.filter.return_value.all.return_value = [mock_setting] mock_session.query.return_value = mock_query # Env has "false" - parse_boolean("false") correctly returns False os.environ["LDR_APP_TEST_FLAG"] = "false" try: manager = SettingsManager(db_session=mock_session) result = manager.get_bool_setting( "app.test_flag", False, check_env=True ) # parse_boolean("false") = False (correct behavior) assert result is False finally: del os.environ["LDR_APP_TEST_FLAG"] def test_get_bool_setting_env_override_with_text_ui_element(self): """Env var 'false' works correctly with text ui_element. When ui_element="text", the setting is returned as string, then get_bool_setting uses to_bool() which correctly handles "false" as False. """ from local_deep_research.settings.manager import ( SettingsManager, ) mock_session = Mock() mock_query = Mock() mock_setting = MockSetting( key="app.test_flag", value="true", ui_element="text" ) mock_query.filter.return_value.all.return_value = [mock_setting] mock_session.query.return_value = mock_query # Env has "false" - with text ui_element this works correctly os.environ["LDR_APP_TEST_FLAG"] = "false" try: manager = SettingsManager(db_session=mock_session) result = manager.get_bool_setting( "app.test_flag", True, check_env=True ) # to_bool("false") correctly returns False assert result is False finally: del os.environ["LDR_APP_TEST_FLAG"] def test_get_bool_setting_no_env_override_when_disabled(self): """get_bool_setting ignores env var when check_env=False.""" from local_deep_research.settings.manager import ( SettingsManager, ) mock_session = Mock() mock_query = Mock() # DB has False mock_setting = MockSetting( key="app.test_flag", value=False, ui_element="checkbox" ) mock_query.filter.return_value.all.return_value = [mock_setting] mock_session.query.return_value = mock_query # Env has "true" but should be ignored os.environ["LDR_APP_TEST_FLAG"] = "true" try: manager = SettingsManager(db_session=mock_session) result = manager.get_bool_setting( "app.test_flag", True, check_env=False ) # Should use DB value, not env assert result is False finally: del os.environ["LDR_APP_TEST_FLAG"] def test_get_bool_setting_env_numeric_string_with_text_ui(self): """get_bool_setting handles env var with '1' or '0' when ui_element=text.""" from local_deep_research.settings.manager import ( SettingsManager, ) mock_session = Mock() mock_query = Mock() # Need a setting with text ui_element for string passthrough mock_setting = MockSetting( key="app.test_flag", value="default", ui_element="text" ) mock_query.filter.return_value.all.return_value = [mock_setting] mock_session.query.return_value = mock_query # Test "1" as truthy - to_bool("1") = True os.environ["LDR_APP_TEST_FLAG"] = "1" try: manager = SettingsManager(db_session=mock_session) result = manager.get_bool_setting( "app.test_flag", False, check_env=True ) assert result is True finally: del os.environ["LDR_APP_TEST_FLAG"] # Test "0" as falsy - to_bool("0") = False os.environ["LDR_APP_TEST_FLAG"] = "0" try: manager = SettingsManager(db_session=mock_session) result = manager.get_bool_setting( "app.test_flag", True, check_env=True ) assert result is False finally: del os.environ["LDR_APP_TEST_FLAG"] class TestSettingsManagerErrorHandling: """ Tests for error handling in SettingsManager methods. These tests verify graceful degradation when database or other components fail. """ def test_get_bool_setting_db_error_returns_default(self): """get_bool_setting returns default when DB query fails.""" from local_deep_research.settings.manager import ( SettingsManager, ) from sqlalchemy.exc import SQLAlchemyError mock_session = Mock() mock_query = Mock() mock_query.filter.side_effect = SQLAlchemyError("DB connection lost") mock_session.query.return_value = mock_query manager = SettingsManager(db_session=mock_session) # Should return default on error, not raise result = manager.get_bool_setting("test.setting", True, check_env=False) assert result is True result = manager.get_bool_setting( "test.setting", False, check_env=False ) assert result is False def test_get_setting_without_db_session_returns_default(self): """get_setting returns default when no DB session available.""" from local_deep_research.settings.manager import ( SettingsManager, ) # No DB session manager = SettingsManager(db_session=None) result = manager.get_setting("test.setting", default="fallback") # Should return default from default_settings or the provided default assert result is not None or result == "fallback" def test_get_bool_setting_without_db_session(self): """get_bool_setting works without DB session using defaults.""" from local_deep_research.settings.manager import ( SettingsManager, ) # No DB session manager = SettingsManager(db_session=None) # Should return the provided default result = manager.get_bool_setting( "nonexistent.setting", True, check_env=False ) assert result is True class TestSettingsManagerBackgroundThreadUsage: """ Tests for SettingsManager usage in background threads. These tests verify that SettingsManager works correctly when used from background threads (like in trigger_auto_index and _get_rag_service_for_thread). """ def test_settings_manager_in_different_thread(self): """SettingsManager can be created and used in a different thread.""" from local_deep_research.settings.manager import ( SettingsManager, ) import threading import queue result_queue = queue.Queue() error_queue = queue.Queue() def thread_func(): try: # Create manager in this thread manager = SettingsManager(db_session=None) # Verify methods exist assert hasattr(manager, "get_bool_setting") assert callable(manager.get_bool_setting) # Call get_bool_setting result = manager.get_bool_setting( "test.setting", True, check_env=False ) result_queue.put(result) except Exception as e: error_queue.put(e) thread = threading.Thread(target=thread_func) thread.start() thread.join(timeout=5) # Check for errors if not error_queue.empty(): raise error_queue.get() # Verify result assert not result_queue.empty() result = result_queue.get() assert isinstance(result, bool) def test_concurrent_get_bool_setting_calls(self): """Multiple threads can call get_bool_setting concurrently.""" from local_deep_research.settings.manager import ( SettingsManager, ) import threading import queue num_threads = 5 results = queue.Queue() errors = queue.Queue() def thread_func(thread_id): try: manager = SettingsManager(db_session=None) for _ in range(10): result = manager.get_bool_setting( f"test.setting.{thread_id}", thread_id % 2 == 0, check_env=False, ) results.put((thread_id, result)) except Exception as e: errors.put((thread_id, e)) threads = [] for i in range(num_threads): t = threading.Thread(target=thread_func, args=(i,)) threads.append(t) t.start() for t in threads: t.join(timeout=10) # Check no errors occurred if not errors.empty(): thread_id, error = errors.get() raise AssertionError(f"Thread {thread_id} failed: {error}") # Verify we got results from all threads assert results.qsize() == num_threads * 10 def test_get_settings_snapshot_in_thread(self): """get_settings_snapshot works correctly in background thread.""" from local_deep_research.settings.manager import ( SettingsManager, ) import threading import queue result_queue = queue.Queue() error_queue = queue.Queue() def thread_func(): try: manager = SettingsManager(db_session=None) snapshot = manager.get_settings_snapshot() # Verify it's a dict assert isinstance(snapshot, dict) # Verify we can modify it (as rag_routes does) snapshot["_username"] = "test_user" assert snapshot["_username"] == "test_user" result_queue.put(snapshot) except Exception as e: error_queue.put(e) thread = threading.Thread(target=thread_func) thread.start() thread.join(timeout=5) if not error_queue.empty(): raise error_queue.get() assert not result_queue.empty() class TestGetSettingsSnapshotAdvanced: """ Advanced tests for get_settings_snapshot() method. These tests cover mutation, complex values, and integration patterns. """ def test_snapshot_mutation_isolation(self): """Modifying snapshot doesn't affect subsequent calls.""" from local_deep_research.settings.manager import ( SettingsManager, ) mock_session = Mock() mock_query = Mock() mock_settings = [ MockSetting(key="test.key", value="original", ui_element="text"), ] mock_query.all.return_value = mock_settings mock_session.query.return_value = mock_query manager = SettingsManager(db_session=mock_session) # Get first snapshot snapshot1 = manager.get_settings_snapshot() # Modify it snapshot1["test.key"] = "modified" snapshot1["new.key"] = "added" # Get second snapshot snapshot2 = manager.get_settings_snapshot() # Second snapshot should not be affected by modifications to first assert snapshot2.get("test.key") == "original" assert "new.key" not in snapshot2 def test_snapshot_with_complex_values(self): """Snapshot preserves complex value types like lists and dicts.""" from local_deep_research.settings.manager import ( SettingsManager, ) mock_session = Mock() mock_query = Mock() mock_settings = [ MockSetting( key="list.setting", value=["item1", "item2", "item3"], ui_element="json", ), MockSetting( key="dict.setting", value={"nested": {"key": "value"}}, ui_element="json", ), ] mock_query.all.return_value = mock_settings mock_session.query.return_value = mock_query manager = SettingsManager(db_session=mock_session) snapshot = manager.get_settings_snapshot() # Verify complex types are preserved assert snapshot["list.setting"] == ["item1", "item2", "item3"] assert snapshot["dict.setting"] == {"nested": {"key": "value"}} def test_snapshot_rag_service_usage_pattern(self): """Test the exact usage pattern from _get_rag_service_for_thread.""" from local_deep_research.settings.manager import ( SettingsManager, ) mock_session = Mock() mock_query = Mock() mock_settings = [ MockSetting( key="local_search_embedding_model", value="all-MiniLM-L6-v2", ui_element="text", ), MockSetting( key="local_search_embedding_provider", value="sentence_transformers", ui_element="text", ), ] mock_query.all.return_value = mock_settings mock_session.query.return_value = mock_query manager = SettingsManager(db_session=mock_session) # This is the exact pattern from rag_routes.py line 2193-2194 settings_snapshot = manager.get_settings_snapshot() settings_snapshot["_username"] = "test_user" # Verify the pattern works assert settings_snapshot["_username"] == "test_user" assert "local_search_embedding_model" in settings_snapshot # Verify it can be passed to other functions def mock_embedding_manager(settings_snapshot): return settings_snapshot.get("local_search_embedding_model") result = mock_embedding_manager(settings_snapshot) assert result == "all-MiniLM-L6-v2"