""" Behavioral tests for settings/manager module. Tests pure functions: parse_boolean, _parse_number, get_typed_setting_value, and check_env_setting. """ import os from unittest.mock import patch import pytest class TestParseBoolean: """Tests for parse_boolean function (HTML checkbox semantics).""" def test_true_returns_true(self): """Bool True returns True.""" from local_deep_research.settings.manager import parse_boolean assert parse_boolean(True) is True def test_false_returns_false(self): """Bool False returns False.""" from local_deep_research.settings.manager import parse_boolean assert parse_boolean(False) is False def test_none_returns_false(self): """None returns False (missing checkbox).""" from local_deep_research.settings.manager import parse_boolean assert parse_boolean(None) is False def test_empty_string_returns_false(self): """Empty string returns False.""" from local_deep_research.settings.manager import parse_boolean assert parse_boolean("") is False def test_false_string_returns_false(self): """'false' returns False.""" from local_deep_research.settings.manager import parse_boolean assert parse_boolean("false") is False def test_off_string_returns_false(self): """'off' returns False.""" from local_deep_research.settings.manager import parse_boolean assert parse_boolean("off") is False def test_zero_string_returns_false(self): """'0' returns False.""" from local_deep_research.settings.manager import parse_boolean assert parse_boolean("0") is False def test_no_string_returns_false(self): """'no' returns False.""" from local_deep_research.settings.manager import parse_boolean assert parse_boolean("no") is False def test_on_returns_true(self): """'on' returns True (HTML checkbox value).""" from local_deep_research.settings.manager import parse_boolean assert parse_boolean("on") is True def test_true_string_returns_true(self): """'true' returns True.""" from local_deep_research.settings.manager import parse_boolean assert parse_boolean("true") is True def test_one_string_returns_true(self): """'1' returns True.""" from local_deep_research.settings.manager import parse_boolean assert parse_boolean("1") is True def test_any_nonempty_string_is_true(self): """Any non-empty non-falsy string returns True (checkbox semantics).""" from local_deep_research.settings.manager import parse_boolean assert parse_boolean("enabled") is True assert parse_boolean("custom_value") is True assert parse_boolean("yes") is True def test_case_insensitive(self): """String comparison is case-insensitive.""" from local_deep_research.settings.manager import parse_boolean assert parse_boolean("FALSE") is False assert parse_boolean("False") is False assert parse_boolean("OFF") is False def test_strips_whitespace(self): """Strips whitespace from string values.""" from local_deep_research.settings.manager import parse_boolean assert parse_boolean(" false ") is False assert parse_boolean(" true ") is True def test_integer_truthy(self): """Non-zero integers return True.""" from local_deep_research.settings.manager import parse_boolean assert parse_boolean(1) is True assert parse_boolean(42) is True def test_integer_zero_is_false(self): """Zero returns False.""" from local_deep_research.settings.manager import parse_boolean assert parse_boolean(0) is False class TestParseJsonValue: """Tests for _parse_json_value function.""" def test_parses_json_array_string(self): """JSON array string is parsed into a list.""" from local_deep_research.settings.manager import _parse_json_value assert _parse_json_value('["general", "news"]') == ["general", "news"] def test_parses_json_array_with_crlf(self): """JSON array string with \\r\\n (browser textarea) is parsed correctly.""" from local_deep_research.settings.manager import _parse_json_value raw = '[\r\n "general"\r\n]' assert _parse_json_value(raw) == ["general"] def test_parses_json_object_string(self): """JSON object string is parsed into a dict.""" from local_deep_research.settings.manager import _parse_json_value assert _parse_json_value('{"key": "value"}') == {"key": "value"} def test_returns_list_unchanged(self): """Already-parsed list is returned as-is.""" from local_deep_research.settings.manager import _parse_json_value original = ["general", "news"] assert _parse_json_value(original) is original def test_returns_dict_unchanged(self): """Already-parsed dict is returned as-is.""" from local_deep_research.settings.manager import _parse_json_value original = {"key": "value"} assert _parse_json_value(original) is original def test_returns_none_unchanged(self): """None is returned as-is.""" from local_deep_research.settings.manager import _parse_json_value assert _parse_json_value(None) is None def test_invalid_json_string_returned_as_is(self): """Non-JSON string is returned unchanged.""" from local_deep_research.settings.manager import _parse_json_value assert _parse_json_value("not json") == "not json" def test_empty_string_returned_as_is(self): """Empty string is returned as-is.""" from local_deep_research.settings.manager import _parse_json_value assert _parse_json_value("") == "" def test_whitespace_only_string_returned_as_is(self): """Whitespace-only string is returned as-is.""" from local_deep_research.settings.manager import _parse_json_value assert _parse_json_value(" ") == " " def test_json_ui_element_uses_parse_json_value(self): """get_typed_setting_value with 'json' ui_element parses JSON strings.""" from local_deep_research.settings.manager import get_typed_setting_value result = get_typed_setting_value( "test.key", '[\r\n "general"\r\n]', "json", check_env=False ) assert result == ["general"] class TestParseNumber: """Tests for _parse_number function.""" def test_whole_float_returns_int(self): """Whole number float string returns int.""" from local_deep_research.settings.manager import _parse_number assert _parse_number("42.0") == 42 assert isinstance(_parse_number("42.0"), int) def test_fractional_float_returns_float(self): """Fractional float string returns float.""" from local_deep_research.settings.manager import _parse_number assert _parse_number("3.14") == pytest.approx(3.14) assert isinstance(_parse_number("3.14"), float) def test_integer_string_returns_int(self): """Integer string returns int.""" from local_deep_research.settings.manager import _parse_number assert _parse_number("10") == 10 assert isinstance(_parse_number("10"), int) def test_negative_number(self): """Handles negative numbers.""" from local_deep_research.settings.manager import _parse_number assert _parse_number("-5") == -5 def test_zero_returns_int(self): """Zero returns int 0.""" from local_deep_research.settings.manager import _parse_number result = _parse_number("0") assert result == 0 assert isinstance(result, int) def test_raises_for_non_numeric(self): """Raises ValueError for non-numeric string.""" from local_deep_research.settings.manager import _parse_number with pytest.raises(ValueError): _parse_number("abc") class TestGetTypedSettingValue: """Tests for get_typed_setting_value function.""" def test_text_returns_string(self): """Text ui_element returns string.""" from local_deep_research.settings.manager import get_typed_setting_value result = get_typed_setting_value( "test.key", "hello", "text", check_env=False ) assert result == "hello" assert isinstance(result, str) def test_number_returns_int_for_whole(self): """Number ui_element returns int for whole number.""" from local_deep_research.settings.manager import get_typed_setting_value result = get_typed_setting_value( "test.key", "42", "number", check_env=False ) assert result == 42 assert isinstance(result, int) def test_number_returns_float_for_fractional(self): """Number ui_element returns float for fractional number.""" from local_deep_research.settings.manager import get_typed_setting_value result = get_typed_setting_value( "test.key", "3.14", "number", check_env=False ) assert result == pytest.approx(3.14) def test_checkbox_true_values(self): """Checkbox ui_element converts truthy values.""" from local_deep_research.settings.manager import get_typed_setting_value assert ( get_typed_setting_value("k", "on", "checkbox", check_env=False) is True ) assert ( get_typed_setting_value("k", "true", "checkbox", check_env=False) is True ) def test_checkbox_false_values(self): """Checkbox ui_element converts falsy values.""" from local_deep_research.settings.manager import get_typed_setting_value assert ( get_typed_setting_value("k", "off", "checkbox", check_env=False) is False ) assert ( get_typed_setting_value("k", "false", "checkbox", check_env=False) is False ) def test_password_returns_string(self): """Password ui_element returns string.""" from local_deep_research.settings.manager import get_typed_setting_value result = get_typed_setting_value( "test.key", "secret123", "password", check_env=False ) assert result == "secret123" def test_select_returns_string(self): """Select ui_element returns string.""" from local_deep_research.settings.manager import get_typed_setting_value result = get_typed_setting_value( "test.key", "option_a", "select", check_env=False ) assert result == "option_a" def test_unknown_ui_element_returns_default(self): """Unknown ui_element returns default value.""" from local_deep_research.settings.manager import get_typed_setting_value result = get_typed_setting_value( "test.key", "value", "unknown_element", default="fallback", check_env=False, ) assert result == "fallback" def test_none_value_returns_default(self): """None value returns default.""" from local_deep_research.settings.manager import get_typed_setting_value result = get_typed_setting_value( "test.key", None, "text", default="default_val", check_env=False ) assert result == "default_val" def test_range_behaves_like_number(self): """Range ui_element behaves like number.""" from local_deep_research.settings.manager import get_typed_setting_value result = get_typed_setting_value( "test.key", "50", "range", check_env=False ) assert result == 50 def test_invalid_number_returns_default(self): """Invalid number string returns default.""" from local_deep_research.settings.manager import get_typed_setting_value result = get_typed_setting_value( "test.key", "not_a_number", "number", default=0, check_env=False ) assert result == 0 class TestCheckEnvSetting: """Tests for check_env_setting function.""" def test_returns_none_when_not_set(self): """Returns None when env var not set.""" from local_deep_research.settings.manager import check_env_setting result = check_env_setting("nonexistent.setting.xyz_test_12345") assert result is None def test_returns_value_when_set(self): """Returns value when env var is set.""" from local_deep_research.settings.manager import check_env_setting with patch.dict(os.environ, {"LDR_MY_TEST_KEY": "test_value"}): result = check_env_setting("my.test_key") assert result == "test_value" def test_env_var_name_format(self): """Env var name is LDR_ + key with dots replaced by underscores, uppercased.""" from local_deep_research.settings.manager import check_env_setting # Key "llm.model" -> env var "LDR_LLM_MODEL" with patch.dict(os.environ, {"LDR_LLM_MODEL": "gpt-4"}): result = check_env_setting("llm.model") assert result == "gpt-4" def test_nested_key_format(self): """Handles deeply nested keys.""" from local_deep_research.settings.manager import check_env_setting with patch.dict( os.environ, {"LDR_SEARCH_BRAVE_API_KEY": "brave-key-123"} ): result = check_env_setting("search.brave.api_key") assert result == "brave-key-123" def test_returns_none_for_empty_string(self): """Empty string env var is treated as unset, returns None, and logs warning.""" from loguru import logger from local_deep_research.settings.manager import check_env_setting warnings = [] logger.enable("local_deep_research") sink_id = logger.add( lambda msg: warnings.append(str(msg)), level="WARNING", # diagnose=False keeps the captured sink consistent with the # production policy (#4185 / #4384) — exceptions logged through # this fixture would otherwise carry frame-local repr() into # the captured list and into pytest output. diagnose=False, ) try: with patch.dict(os.environ, {"LDR_LLM_PROVIDER": ""}): result = check_env_setting("llm.provider") assert result is None assert any("LDR_LLM_PROVIDER" in w for w in warnings) finally: logger.remove(sink_id) logger.disable("local_deep_research") def test_empty_string_provider_key(self): """Empty LDR_LLM_PROVIDER returns None (Unraid scenario).""" from local_deep_research.settings.manager import check_env_setting with patch.dict(os.environ, {"LDR_LLM_PROVIDER": ""}): result = check_env_setting("llm.provider") assert result is None def test_empty_string_model_key(self): """Empty LDR_LLM_MODEL returns None.""" from local_deep_research.settings.manager import check_env_setting with patch.dict(os.environ, {"LDR_LLM_MODEL": ""}): result = check_env_setting("llm.model") assert result is None def test_empty_string_multiple_keys(self): """Multiple empty env vars each independently return None.""" from local_deep_research.settings.manager import check_env_setting with patch.dict( os.environ, {"LDR_LLM_PROVIDER": "", "LDR_LLM_MODEL": ""}, ): assert check_env_setting("llm.provider") is None assert check_env_setting("llm.model") is None class TestUIElementToSettingType: """Tests for UI_ELEMENT_TO_SETTING_TYPE mapping.""" def test_has_text(self): """Mapping includes 'text'.""" from local_deep_research.settings.manager import ( UI_ELEMENT_TO_SETTING_TYPE, ) assert "text" in UI_ELEMENT_TO_SETTING_TYPE def test_has_checkbox(self): """Mapping includes 'checkbox'.""" from local_deep_research.settings.manager import ( UI_ELEMENT_TO_SETTING_TYPE, ) assert "checkbox" in UI_ELEMENT_TO_SETTING_TYPE def test_has_number(self): """Mapping includes 'number'.""" from local_deep_research.settings.manager import ( UI_ELEMENT_TO_SETTING_TYPE, ) assert "number" in UI_ELEMENT_TO_SETTING_TYPE def test_has_password(self): """Mapping includes 'password'.""" from local_deep_research.settings.manager import ( UI_ELEMENT_TO_SETTING_TYPE, ) assert "password" in UI_ELEMENT_TO_SETTING_TYPE def test_has_select(self): """Mapping includes 'select'.""" from local_deep_research.settings.manager import ( UI_ELEMENT_TO_SETTING_TYPE, ) assert "select" in UI_ELEMENT_TO_SETTING_TYPE def test_has_json(self): """Mapping includes 'json'.""" from local_deep_research.settings.manager import ( UI_ELEMENT_TO_SETTING_TYPE, ) assert "json" in UI_ELEMENT_TO_SETTING_TYPE def test_has_range(self): """Mapping includes 'range'.""" from local_deep_research.settings.manager import ( UI_ELEMENT_TO_SETTING_TYPE, ) assert "range" in UI_ELEMENT_TO_SETTING_TYPE def test_text_type_is_str(self): """Text type is str.""" from local_deep_research.settings.manager import ( UI_ELEMENT_TO_SETTING_TYPE, ) assert UI_ELEMENT_TO_SETTING_TYPE["text"] is str class TestParseNumberEdgeCases: """Edge cases for _parse_number that affect real settings.""" def test_inf_string_returns_float_inf(self): """'inf' parses to float — callers must validate range themselves.""" from local_deep_research.settings.manager import _parse_number result = _parse_number("inf") assert result == float("inf") assert isinstance(result, float) def test_negative_inf_string(self): """'-inf' parses to negative infinity.""" from local_deep_research.settings.manager import _parse_number result = _parse_number("-inf") assert result == float("-inf") def test_nan_string_returns_float_nan(self): """'nan' parses to float NaN.""" import math from local_deep_research.settings.manager import _parse_number result = _parse_number("nan") assert math.isnan(result) def test_int_input_passthrough(self): """Direct int input is preserved as int (not round-tripped through float).""" from local_deep_research.settings.manager import _parse_number result = _parse_number(42) assert result == 42 assert isinstance(result, int) def test_float_input_whole_becomes_int(self): """Float 5.0 becomes int 5 (is_integer() check).""" from local_deep_research.settings.manager import _parse_number result = _parse_number(5.0) assert result == 5 assert isinstance(result, int) def test_scientific_notation(self): """Scientific notation string parses correctly.""" from local_deep_research.settings.manager import _parse_number result = _parse_number("1e3") assert result == 1000 assert isinstance(result, int) def test_very_large_number(self): """Large number beyond typical int range still works.""" from local_deep_research.settings.manager import _parse_number result = _parse_number("99999999999999") assert result == 99999999999999 assert isinstance(result, int) class TestParseMultiselectEdgeCases: """Edge cases for _parse_multiselect with non-standard inputs.""" def test_dict_input_returned_as_is(self): """Dict passes through unchanged (not list, not string).""" from local_deep_research.settings.manager import _parse_multiselect d = {"key": "value"} assert _parse_multiselect(d) is d def test_int_input_returned_as_is(self): """Integer passes through unchanged.""" from local_deep_research.settings.manager import _parse_multiselect assert _parse_multiselect(42) == 42 def test_none_input_returned_as_is(self): """None passes through unchanged.""" from local_deep_research.settings.manager import _parse_multiselect assert _parse_multiselect(None) is None def test_json_array_of_non_list_falls_back_to_csv(self): """JSON string that parses to non-list falls through to CSV split.""" from local_deep_research.settings.manager import _parse_multiselect # Starts with '[' but json.loads returns... actually this is a list # Use a case where JSON parse succeeds but isn't a list: impossible # with '[' prefix. Test that invalid JSON starting with '[' falls to CSV. result = _parse_multiselect("[not valid json") assert result == ["[not valid json"] def test_empty_string_returns_empty_list(self): """Empty string returns empty list after CSV split filtering.""" from local_deep_research.settings.manager import _parse_multiselect assert _parse_multiselect("") == [] def test_whitespace_only_items_filtered(self): """Comma-separated with blank items filters them out.""" from local_deep_research.settings.manager import _parse_multiselect result = _parse_multiselect("a, , b, ,c") assert result == ["a", "b", "c"] class TestParseJsonValueEdgeCases: """Edge cases for _parse_json_value with tricky inputs.""" def test_json_number_string_parses_to_number(self): """A plain number string is valid JSON — parses to int.""" from local_deep_research.settings.manager import _parse_json_value result = _parse_json_value("42") assert result == 42 def test_json_boolean_string_parses_to_bool(self): """'true' is valid JSON — parses to Python True.""" from local_deep_research.settings.manager import _parse_json_value assert _parse_json_value("true") is True assert _parse_json_value("false") is False def test_json_null_string_parses_to_none(self): """'null' is valid JSON — parses to None.""" from local_deep_research.settings.manager import _parse_json_value assert _parse_json_value("null") is None def test_nested_json_object(self): """Nested JSON objects parse correctly.""" from local_deep_research.settings.manager import _parse_json_value raw = '{"outer": {"inner": [1, 2]}}' result = _parse_json_value(raw) assert result == {"outer": {"inner": [1, 2]}} def test_int_input_passthrough(self): """Non-string types pass through unchanged.""" from local_deep_research.settings.manager import _parse_json_value assert _parse_json_value(42) == 42 def test_bool_input_passthrough(self): """Boolean passes through unchanged.""" from local_deep_research.settings.manager import _parse_json_value assert _parse_json_value(True) is True def test_json_with_leading_whitespace(self): """JSON string with leading whitespace is stripped before parsing.""" from local_deep_research.settings.manager import _parse_json_value result = _parse_json_value(' {"key": "value"} ') assert result == {"key": "value"} class TestGetTypedSettingValueEnvFallback: """Tests for env var → DB fallback path in get_typed_setting_value.""" def test_invalid_env_var_falls_back_to_db_value(self): """When env var has invalid value for the type, DB value is used.""" from local_deep_research.settings.manager import get_typed_setting_value with patch.dict(os.environ, {"LDR_TEST_ITERATIONS": "not_a_number"}): result = get_typed_setting_value( "test.iterations", 5, "number", default=0, check_env=True ) # Should fall back to DB value 5, not env "not_a_number" assert result == 5 def test_valid_env_var_overrides_db_value(self): """When env var is valid, it takes priority over DB value.""" from local_deep_research.settings.manager import get_typed_setting_value with patch.dict(os.environ, {"LDR_TEST_ITERATIONS": "10"}): result = get_typed_setting_value( "test.iterations", 5, "number", default=0, check_env=True ) assert result == 10 def test_env_var_skipped_when_check_env_false(self): """check_env=False ignores environment variables entirely.""" from local_deep_research.settings.manager import get_typed_setting_value with patch.dict(os.environ, {"LDR_TEST_ITERATIONS": "99"}): result = get_typed_setting_value( "test.iterations", 5, "number", default=0, check_env=False ) assert result == 5 def test_env_var_type_error_on_number_does_not_crash(self): """TypeError during env var conversion falls through to DB value. Note: the env path only catches ValueError (line 206), so a TypeError would propagate. This documents the current behavior — _parse_number with a string always raises ValueError, never TypeError. """ from local_deep_research.settings.manager import get_typed_setting_value # _parse_number("") raises ValueError, not TypeError with patch.dict(os.environ, {"LDR_TEST_NUM": ""}): result = get_typed_setting_value( "test.num", 42, "number", default=0, check_env=True ) assert result == 42 def test_db_value_type_error_returns_default(self): """TypeError during DB value conversion returns default. This path (line 220) catches both ValueError and TypeError, unlike the env path which only catches ValueError. """ from local_deep_research.settings.manager import get_typed_setting_value # Passing a type that causes TypeError in float() — e.g. a list result = get_typed_setting_value( "test.key", [1, 2, 3], "number", default=0, check_env=False ) assert result == 0 def test_textarea_preserves_multiline_string(self): """Textarea ui_element preserves newlines (uses str converter).""" from local_deep_research.settings.manager import get_typed_setting_value multiline = "line1\nline2\nline3" result = get_typed_setting_value( "test.key", multiline, "textarea", check_env=False ) assert result == multiline def test_number_with_bool_input_converts_to_int(self): """Bool passed to number field: float(True)=1.0, is_integer()→int 1. This is a real scenario when frontend sends JSON true for a number field. """ from local_deep_research.settings.manager import get_typed_setting_value result = get_typed_setting_value( "test.key", True, "number", check_env=False ) assert result == 1 assert isinstance(result, int) def test_checkbox_with_int_zero(self): """Integer 0 sent for a checkbox field returns False.""" from local_deep_research.settings.manager import get_typed_setting_value result = get_typed_setting_value( "test.key", 0, "checkbox", check_env=False ) assert result is False def test_checkbox_with_int_one(self): """Integer 1 sent for a checkbox field returns True.""" from local_deep_research.settings.manager import get_typed_setting_value result = get_typed_setting_value( "test.key", 1, "checkbox", check_env=False ) assert result is True