feat(config): 统一配置加载入口,启动时校验所有YAML配置

将分散的 config.yml、workflow.yaml、sandbox.yaml 加载逻辑统一到 AppConfig 模型,
启动时一次性校验,失败则 fast-fail。sandbox.py 改为从统一配置取值,消除重复加载。
同时修复 onebot 测试并新增14个统一配置测试(总测试 285→300)。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-03 13:52:03 +00:00
parent 80174acaae
commit 76a67e8237
8 changed files with 358 additions and 116 deletions
+168
View File
@@ -0,0 +1,168 @@
"""统一配置加载 (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