diff --git a/src/config/loader.py b/src/config/loader.py index a1488c8..6f7d46e 100644 --- a/src/config/loader.py +++ b/src/config/loader.py @@ -12,7 +12,7 @@ def replace_env_vars(value: str) -> str: return value if value.startswith("$"): env_var = value[1:] - return os.getenv(env_var, value) + return os.getenv(env_var, env_var) return value diff --git a/tests/test_state.py b/tests/test_state.py index f65681b..f196891 100644 --- a/tests/test_state.py +++ b/tests/test_state.py @@ -1,3 +1,6 @@ +# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates +# SPDX-License-Identifier: MIT + import pytest import sys import os diff --git a/tests/unit/config/test_configuration.py b/tests/unit/config/test_configuration.py new file mode 100644 index 0000000..a70aa79 --- /dev/null +++ b/tests/unit/config/test_configuration.py @@ -0,0 +1,95 @@ +# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates +# SPDX-License-Identifier: MIT + +import os +import pytest +import sys +import types +from pathlib import Path +import builtins +import importlib +from src.config.configuration import Configuration + +# Patch sys.path so relative import works + +# Patch Resource for import +mock_resource = type("Resource", (), {}) + +# Patch src.rag.retriever.Resource for import + +module_name = "src.rag.retriever" +if module_name not in sys.modules: + retriever_mod = types.ModuleType(module_name) + retriever_mod.Resource = mock_resource + sys.modules[module_name] = retriever_mod + +# Relative import of Configuration + + +def test_default_configuration(): + config = Configuration() + assert config.resources == [] + assert config.max_plan_iterations == 1 + assert config.max_step_num == 3 + assert config.max_search_results == 3 + assert config.mcp_settings is None + + +def test_from_runnable_config_with_config_dict(monkeypatch): + config_dict = { + "configurable": { + "max_plan_iterations": 5, + "max_step_num": 7, + "max_search_results": 10, + "mcp_settings": {"foo": "bar"}, + } + } + config = Configuration.from_runnable_config(config_dict) + assert config.max_plan_iterations == 5 + assert config.max_step_num == 7 + assert config.max_search_results == 10 + assert config.mcp_settings == {"foo": "bar"} + + +def test_from_runnable_config_with_env_override(monkeypatch): + monkeypatch.setenv("MAX_PLAN_ITERATIONS", "9") + monkeypatch.setenv("MAX_STEP_NUM", "11") + config_dict = { + "configurable": { + "max_plan_iterations": 2, + "max_step_num": 3, + "max_search_results": 4, + } + } + config = Configuration.from_runnable_config(config_dict) + # Environment variables take precedence and are strings + assert config.max_plan_iterations == "9" + assert config.max_step_num == "11" + assert config.max_search_results == 4 # not overridden + # Clean up + monkeypatch.delenv("MAX_PLAN_ITERATIONS") + monkeypatch.delenv("MAX_STEP_NUM") + + +def test_from_runnable_config_with_none_and_falsy(monkeypatch): + config_dict = { + "configurable": { + "max_plan_iterations": None, + "max_step_num": 0, # falsy, should be skipped + "max_search_results": "", + } + } + config = Configuration.from_runnable_config(config_dict) + # Should fall back to defaults for skipped/falsy values + assert config.max_plan_iterations == 1 + assert config.max_step_num == 3 + assert config.max_search_results == 3 + + +def test_from_runnable_config_with_no_config(): + config = Configuration.from_runnable_config() + assert config.max_plan_iterations == 1 + assert config.max_step_num == 3 + assert config.max_search_results == 3 + assert config.resources == [] + assert config.mcp_settings is None diff --git a/tests/unit/config/test_loader.py b/tests/unit/config/test_loader.py new file mode 100644 index 0000000..aaf75af --- /dev/null +++ b/tests/unit/config/test_loader.py @@ -0,0 +1,83 @@ +# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates +# SPDX-License-Identifier: MIT + +import os +import tempfile +import yaml +import pytest +from src.config.loader import load_yaml_config, process_dict, replace_env_vars + + +def test_replace_env_vars_with_env(monkeypatch): + monkeypatch.setenv("TEST_ENV", "env_value") + assert replace_env_vars("$TEST_ENV") == "env_value" + + +def test_replace_env_vars_without_env(monkeypatch): + monkeypatch.delenv("NOT_SET_ENV", raising=False) + assert replace_env_vars("$NOT_SET_ENV") == "NOT_SET_ENV" + + +def test_replace_env_vars_non_string(): + assert replace_env_vars(123) == 123 + + +def test_replace_env_vars_regular_string(): + assert replace_env_vars("no_env") == "no_env" + + +def test_process_dict_nested(monkeypatch): + monkeypatch.setenv("FOO", "bar") + config = {"a": "$FOO", "b": {"c": "$FOO", "d": 42, "e": "$NOT_SET_ENV"}} + processed = process_dict(config) + assert processed["a"] == "bar" + assert processed["b"]["c"] == "bar" + assert processed["b"]["d"] == 42 + assert processed["b"]["e"] == "NOT_SET_ENV" + + +def test_process_dict_empty(): + assert process_dict({}) == {} + + +def test_load_yaml_config_file_not_exist(): + assert load_yaml_config("non_existent_file.yaml") == {} + + +def test_load_yaml_config(monkeypatch): + monkeypatch.setenv("MY_ENV", "my_value") + yaml_content = """ + key1: value1 + key2: $MY_ENV + nested: + key3: $MY_ENV + key4: 123 + """ + with tempfile.NamedTemporaryFile("w+", delete=False) as tmp: + tmp.write(yaml_content) + tmp_path = tmp.name + + try: + config = load_yaml_config(tmp_path) + assert config["key1"] == "value1" + assert config["key2"] == "my_value" + assert config["nested"]["key3"] == "my_value" + assert config["nested"]["key4"] == 123 + finally: + os.remove(tmp_path) + + +def test_load_yaml_config_cache(monkeypatch): + monkeypatch.setenv("CACHE_ENV", "cache_value") + yaml_content = "foo: $CACHE_ENV" + with tempfile.NamedTemporaryFile("w+", delete=False) as tmp: + tmp.write(yaml_content) + tmp_path = tmp.name + + try: + config1 = load_yaml_config(tmp_path) + config2 = load_yaml_config(tmp_path) + assert config1 is config2 # Should be cached (same object) + assert config1["foo"] == "cache_value" + finally: + os.remove(tmp_path)