"""U4 验证:10 个业务 Skill YAML 的 preconditions 字段加载正确。 验证项: - 全部 16 个 skill YAML 可被 SkillConfig.from_dict 正常加载 - 10 个业务 skill 的 preconditions 字段非空且为 list[str] - 6 个引擎模板的 preconditions 字段为 None(未配置) """ from __future__ import annotations from pathlib import Path import pytest import yaml from agentkit.skills.base import SkillConfig _SKILLS_DIR = Path(__file__).resolve().parents[2] / "configs" / "skills" # 10 个业务 skill(应配置 preconditions) _BUSINESS_SKILLS = { "code_reviewer", "geo_optimizer", "content_generator", "competitor_analyzer", "benchmark_runner", "trend_agent", "monitor", "citation_detector", "schema_advisor", "deai_agent", } # 6 个引擎模板(不应配置 preconditions) _ENGINE_TEMPLATES = { "react_agent", "direct_agent", "rewoo_agent", "reflexion_agent", "plan_exec_agent", "goal_driven_agent", } def _load_all_skill_configs() -> dict[str, SkillConfig]: """加载 configs/skills/ 下全部 YAML 为 SkillConfig。""" result: dict[str, SkillConfig] = {} for yaml_path in sorted(_SKILLS_DIR.glob("*.yaml")): with yaml_path.open("r", encoding="utf-8") as f: data = yaml.safe_load(f) if not isinstance(data, dict) or "name" not in data: continue config = SkillConfig.from_dict(data) result[config.name] = config return result class TestBusinessSkillPreconditions: """U4:业务 skill preconditions 字段验证。""" def test_all_16_skills_load_without_error(self) -> None: """全部 16 个 skill YAML 可被 SkillConfig.from_dict 正常加载。""" configs = _load_all_skill_configs() assert len(configs) == 16, f"期望 16 个 skill,实际加载 {len(configs)} 个" def test_business_skills_have_non_empty_preconditions(self) -> None: """10 个业务 skill 的 preconditions 字段非空且为 list[str]。""" configs = _load_all_skill_configs() missing = _BUSINESS_SKILLS - set(configs.keys()) assert not missing, f"缺少业务 skill: {missing}" for name in _BUSINESS_SKILLS: config = configs[name] assert config.preconditions is not None, f"{name}.preconditions 为 None" assert isinstance(config.preconditions, list), ( f"{name}.preconditions 不是 list" ) assert len(config.preconditions) >= 2, ( f"{name}.preconditions 少于 2 条(实际 {len(config.preconditions)} 条)" ) assert all(isinstance(p, str) and p.strip() for p in config.preconditions), ( f"{name}.preconditions 存在非字符串或空字符串项" ) def test_engine_templates_have_no_preconditions(self) -> None: """6 个引擎模板的 preconditions 字段为 None(未配置)。""" configs = _load_all_skill_configs() missing = _ENGINE_TEMPLATES - set(configs.keys()) assert not missing, f"缺少引擎模板: {missing}" for name in _ENGINE_TEMPLATES: config = configs[name] assert config.preconditions is None, ( f"引擎模板 {name} 不应配置 preconditions,实际为 {config.preconditions}" ) def test_preconditions_round_trip_through_to_dict(self) -> None: """preconditions 字段经 to_dict 序列化后保持一致。""" configs = _load_all_skill_configs() for name in _BUSINESS_SKILLS: config = configs[name] dumped = config.to_dict() assert dumped.get("preconditions") == config.preconditions, ( f"{name}.to_dict() 的 preconditions 与原值不一致" ) def test_code_reviewer_preconditions_content(self) -> None: """code_reviewer 的 preconditions 包含 shell 工具使用约束。""" configs = _load_all_skill_configs() cr = configs["code_reviewer"] joined = " ".join(cr.preconditions) assert "shell" in joined.lower() or "读取" in joined, ( "code_reviewer preconditions 应包含 shell 工具使用约束" )