"""统一配置加载 (AppConfig) 的测试:多文件联合加载、缺失文件默认值、schema 校验、热重载。""" from __future__ import annotations import yaml import pytest from kilostar.utils.config_loader import ( AppConfig, AppInfo, WorkflowConfig, RetryConfig, SandboxConfig, load_app_config, get_app_config, reload_app_config, get_workflow_config, reload_workflow_config, save_workflow_config, ) @pytest.fixture(autouse=True) def isolated_config(tmp_path, monkeypatch): """隔离配置目录,每个测试用独立临时目录。""" from kilostar.utils import config_loader monkeypatch.setattr(config_loader, "_CONFIG_DIR", tmp_path) monkeypatch.setattr(config_loader, "_WORKFLOW_YAML", tmp_path / "workflow.yaml") monkeypatch.setattr(config_loader, "_CONFIG_YML", tmp_path / "config.yml") monkeypatch.setattr(config_loader, "_SANDBOX_YAML", tmp_path / "sandbox.yaml") monkeypatch.setattr(config_loader, "_app_current", None) return tmp_path # ─── 多文件联合加载 ─── class TestLoadAppConfig: def test_all_files_present(self, isolated_config): (isolated_config / "config.yml").write_text( "version: v0.2.0\nname: TestStar\n", encoding="utf-8" ) (isolated_config / "workflow.yaml").write_text( "retry:\n max_attempts: 10\n", encoding="utf-8" ) (isolated_config / "sandbox.yaml").write_text( "sandbox:\n enabled: false\n", encoding="utf-8" ) cfg = load_app_config(isolated_config) assert cfg.app.version == "v0.2.0" assert cfg.app.name == "TestStar" assert cfg.workflow.retry.max_attempts == 10 assert cfg.sandbox.enabled is False def test_no_files_returns_defaults(self, isolated_config): cfg = load_app_config(isolated_config) assert cfg.app.version == "0.1.1-alpha" assert cfg.app.name == "Kilostar" assert cfg.workflow.retry.max_attempts == 5 assert cfg.sandbox.enabled is True def test_partial_files_fills_defaults(self, isolated_config): (isolated_config / "workflow.yaml").write_text( "retry:\n max_attempts: 8\n", encoding="utf-8" ) cfg = load_app_config(isolated_config) assert cfg.workflow.retry.max_attempts == 8 assert cfg.app.version == "0.1.1-alpha" assert cfg.sandbox.enabled is True def test_sandbox_without_wrapper_key(self, isolated_config): """sandbox.yaml 不带顶层 'sandbox:' key 也能正常解析。""" (isolated_config / "sandbox.yaml").write_text( "enabled: false\n", encoding="utf-8" ) cfg = load_app_config(isolated_config) assert cfg.sandbox.enabled is False # ─── Schema 校验 ─── class TestSchemaValidation: def test_invalid_retry_max_attempts_type(self, isolated_config): (isolated_config / "workflow.yaml").write_text( "retry:\n max_attempts: not_a_number\n", encoding="utf-8" ) with pytest.raises(Exception): load_app_config(isolated_config) def test_retry_max_attempts_out_of_range(self, isolated_config): (isolated_config / "workflow.yaml").write_text( "retry:\n max_attempts: 999\n", encoding="utf-8" ) with pytest.raises(Exception): load_app_config(isolated_config) def test_config_yml_extra_fields_ignored(self, isolated_config): (isolated_config / "config.yml").write_text( "version: v1.0.0\nname: X\nunknown_field: hello\n", encoding="utf-8" ) cfg = load_app_config(isolated_config) assert cfg.app.version == "v1.0.0" # ─── 单例与热重载 ─── class TestSingletonAndReload: def test_get_app_config_is_singleton(self, isolated_config): cfg1 = get_app_config() cfg2 = get_app_config() assert cfg1 is cfg2 def test_reload_full(self, isolated_config): get_app_config() (isolated_config / "workflow.yaml").write_text( "retry:\n max_attempts: 42\n", encoding="utf-8" ) cfg = reload_app_config() assert cfg.workflow.retry.max_attempts == 42 def test_reload_section_sandbox(self, isolated_config): get_app_config() (isolated_config / "sandbox.yaml").write_text( "sandbox:\n enabled: false\n", encoding="utf-8" ) cfg = reload_app_config(section="sandbox") assert cfg.sandbox.enabled is False assert cfg.workflow.retry.max_attempts == 5 def test_reload_section_workflow(self, isolated_config): get_app_config() (isolated_config / "workflow.yaml").write_text( "retry:\n max_attempts: 77\n", encoding="utf-8" ) cfg = reload_app_config(section="workflow") assert cfg.workflow.retry.max_attempts == 77 # ─── 向后兼容性 ─── class TestBackwardCompat: def test_get_workflow_config_delegates(self, isolated_config): (isolated_config / "workflow.yaml").write_text( "retry:\n max_attempts: 15\n", encoding="utf-8" ) wf = get_workflow_config() assert wf.retry.max_attempts == 15 def test_reload_workflow_config_refreshes(self, isolated_config): get_workflow_config() (isolated_config / "workflow.yaml").write_text( "retry:\n max_attempts: 33\n", encoding="utf-8" ) wf = reload_workflow_config() assert wf.retry.max_attempts == 33 def test_save_workflow_config_persists(self, isolated_config): save_workflow_config(WorkflowConfig(retry=RetryConfig(max_attempts=25))) on_disk = yaml.safe_load( (isolated_config / "workflow.yaml").read_text(encoding="utf-8") ) assert on_disk == {"retry": {"max_attempts": 25}} assert get_workflow_config().retry.max_attempts == 25