From 53747fc1f0dbc8ef0340b4866582381f8878a4c7 Mon Sep 17 00:00:00 2001 From: huanghuoguoguo <1051233107@qq.com> Date: Fri, 8 May 2026 15:08:35 +0800 Subject: [PATCH] test(unit): add config loader unit tests U-005: Config Loader tests - Valid YAML config loading - Valid JSON config loading - Invalid YAML/JSON error behavior - Missing config file creation from template - Template completion for missing keys - ConfigManager load/dump operations - Exists check for both YAML and JSON All tests use tmp_path fixture, no real project config. Co-Authored-By: Claude Opus 4.7 --- tests/unit_tests/config/test_config_loader.py | 310 ++++++++++++++++++ 1 file changed, 310 insertions(+) create mode 100644 tests/unit_tests/config/test_config_loader.py diff --git a/tests/unit_tests/config/test_config_loader.py b/tests/unit_tests/config/test_config_loader.py new file mode 100644 index 00000000..9c7fc745 --- /dev/null +++ b/tests/unit_tests/config/test_config_loader.py @@ -0,0 +1,310 @@ +""" +Unit tests for configuration loading and overrides. + +Tests cover: +- Valid YAML config loading +- Valid JSON config loading +- Invalid YAML/JSON error behavior +- Missing config file behavior +- Template completion +""" + +from __future__ import annotations + +import pytest +import os +import json + +from langbot.pkg.config.impls.yaml import YAMLConfigFile +from langbot.pkg.config.impls.json import JSONConfigFile +from langbot.pkg.config.manager import ConfigManager + + +class TestYAMLConfigFile: + """Tests for YAML config file handling.""" + + @pytest.mark.asyncio + async def test_valid_yaml_loads(self, tmp_path): + """Valid YAML config should load correctly.""" + config_file = tmp_path / "test_config.yaml" + + # Write valid YAML + config_file.write_text(""" +name: test_app +version: 1.0 +settings: + debug: true + port: 8080 +""") + + yaml_file = YAMLConfigFile( + str(config_file), + template_data={'name': 'default', 'version': '0.1'}, + ) + + result = await yaml_file.load(completion=False) + + assert result['name'] == 'test_app' + assert result['version'] == 1.0 + assert result['settings']['debug'] is True + assert result['settings']['port'] == 8080 + + @pytest.mark.asyncio + async def test_invalid_yaml_raises_error(self, tmp_path): + """Invalid YAML should raise clear error.""" + config_file = tmp_path / "invalid.yaml" + + # Write invalid YAML (unclosed bracket) + config_file.write_text(""" +name: test +settings: + - item1 + - item2 + - [unclosed +""") + + yaml_file = YAMLConfigFile( + str(config_file), + template_data={'name': 'default'}, + ) + + with pytest.raises(Exception, match="Syntax error"): + await yaml_file.load(completion=False) + + @pytest.mark.asyncio + async def test_missing_config_creates_from_template(self, tmp_path): + """Missing config file should be created from template.""" + config_file = tmp_path / "new_config.yaml" + + # File doesn't exist yet + assert not config_file.exists() + + yaml_file = YAMLConfigFile( + str(config_file), + template_data={'name': 'new_app', 'version': '1.0'}, + ) + + result = await yaml_file.load() + + assert config_file.exists() + assert result['name'] == 'new_app' + assert result['version'] == '1.0' + + @pytest.mark.asyncio + async def test_template_completion(self, tmp_path): + """Config should be completed with template defaults.""" + config_file = tmp_path / "partial.yaml" + + # Write partial config missing some template keys + config_file.write_text(""" +name: custom_name +""") + + yaml_file = YAMLConfigFile( + str(config_file), + template_data={'name': 'default_name', 'version': '2.0', 'debug': False}, + ) + + result = await yaml_file.load(completion=True) + + # Existing key preserved + assert result['name'] == 'custom_name' + # Missing keys filled from template + assert result['version'] == '2.0' + assert result['debug'] is False + + @pytest.mark.asyncio + async def test_yaml_save(self, tmp_path): + """YAML config can be saved.""" + config_file = tmp_path / "save_test.yaml" + + yaml_file = YAMLConfigFile( + str(config_file), + template_data={'name': 'test'}, + ) + + await yaml_file.save({'name': 'saved_app', 'new_key': 'new_value'}) + + assert config_file.exists() + content = config_file.read_text() + assert 'saved_app' in content + assert 'new_key' in content + + def test_yaml_save_sync(self, tmp_path): + """YAML config can be saved synchronously.""" + config_file = tmp_path / "sync_save.yaml" + + yaml_file = YAMLConfigFile( + str(config_file), + template_data={'name': 'test'}, + ) + + yaml_file.save_sync({'name': 'sync_saved'}) + + assert config_file.exists() + content = config_file.read_text() + assert 'sync_saved' in content + + +class TestJSONConfigFile: + """Tests for JSON config file handling.""" + + @pytest.mark.asyncio + async def test_valid_json_loads(self, tmp_path): + """Valid JSON config should load correctly.""" + config_file = tmp_path / "test_config.json" + + # Write valid JSON + config_file.write_text(json.dumps({ + 'name': 'json_app', + 'version': '1.0', + 'settings': {'debug': True, 'port': 8080}, + })) + + json_file = JSONConfigFile( + str(config_file), + template_data={'name': 'default', 'version': '0.1'}, + ) + + result = await json_file.load(completion=False) + + assert result['name'] == 'json_app' + assert result['version'] == '1.0' + assert result['settings']['debug'] is True + + @pytest.mark.asyncio + async def test_invalid_json_raises_error(self, tmp_path): + """Invalid JSON should raise clear error.""" + config_file = tmp_path / "invalid.json" + + # Write invalid JSON (missing closing brace) + config_file.write_text('{"name": "test", "unclosed": ') + + json_file = JSONConfigFile( + str(config_file), + template_data={'name': 'default'}, + ) + + with pytest.raises(Exception, match="Syntax error"): + await json_file.load(completion=False) + + @pytest.mark.asyncio + async def test_missing_json_creates_from_template(self, tmp_path): + """Missing JSON file should be created from template.""" + config_file = tmp_path / "new_config.json" + + json_file = JSONConfigFile( + str(config_file), + template_data={'name': 'new_json_app', 'version': '1.0'}, + ) + + result = await json_file.load() + + assert config_file.exists() + assert result['name'] == 'new_json_app' + + @pytest.mark.asyncio + async def test_json_save(self, tmp_path): + """JSON config can be saved.""" + config_file = tmp_path / "save_test.json" + + json_file = JSONConfigFile( + str(config_file), + template_data={'name': 'test'}, + ) + + await json_file.save({'name': 'saved_json', 'new_key': 'value'}) + + assert config_file.exists() + content = config_file.read_text() + data = json.loads(content) + assert data['name'] == 'saved_json' + + +class TestConfigManager: + """Tests for ConfigManager.""" + + @pytest.mark.asyncio + async def test_config_manager_load(self, tmp_path): + """ConfigManager loads config correctly.""" + config_file = tmp_path / "manager_test.yaml" + config_file.write_text('name: managed_app\nversion: "1.0"\n') + + yaml_file = YAMLConfigFile( + str(config_file), + template_data={'name': 'default', 'version': '0.1'}, + ) + + manager = ConfigManager(yaml_file) + await manager.load_config() + + assert manager.data['name'] == 'managed_app' + assert manager.data['version'] == '1.0' + + @pytest.mark.asyncio + async def test_config_manager_dump(self, tmp_path): + """ConfigManager can dump config.""" + config_file = tmp_path / "dump_test.yaml" + + yaml_file = YAMLConfigFile( + str(config_file), + template_data={'name': 'default'}, + ) + + manager = ConfigManager(yaml_file) + manager.data = {'name': 'dumped', 'new_field': 'value'} + + await manager.dump_config() + + content = config_file.read_text() + assert 'dumped' in content + + def test_config_manager_dump_sync(self, tmp_path): + """ConfigManager can dump config synchronously.""" + config_file = tmp_path / "sync_dump.yaml" + + yaml_file = YAMLConfigFile( + str(config_file), + template_data={'name': 'default'}, + ) + + manager = ConfigManager(yaml_file) + manager.data = {'name': 'sync_dumped'} + + manager.dump_config_sync() + + assert config_file.exists() + + +class TestConfigExists: + """Tests for config file existence check.""" + + def test_yaml_exists_true(self, tmp_path): + """exists() returns True for existing file.""" + config_file = tmp_path / "exists.yaml" + config_file.write_text('name: test') + + yaml_file = YAMLConfigFile(str(config_file), template_data={}) + assert yaml_file.exists() is True + + def test_yaml_exists_false(self, tmp_path): + """exists() returns False for missing file.""" + config_file = tmp_path / "missing.yaml" + + yaml_file = YAMLConfigFile(str(config_file), template_data={}) + assert yaml_file.exists() is False + + def test_json_exists_true(self, tmp_path): + """exists() returns True for existing JSON file.""" + config_file = tmp_path / "exists.json" + config_file.write_text('{}') + + json_file = JSONConfigFile(str(config_file), template_data={}) + assert json_file.exists() is True + + def test_json_exists_false(self, tmp_path): + """exists() returns False for missing JSON file.""" + config_file = tmp_path / "missing.json" + + json_file = JSONConfigFile(str(config_file), template_data={}) + assert json_file.exists() is False \ No newline at end of file