"""SkillConfig 单元测试""" import os import tempfile import pytest import yaml from agentkit.core.exceptions import ConfigValidationError from agentkit.skills.base import IntentConfig, QualityGateConfig, SkillConfig, Skill # ── IntentConfig 测试 ────────────────────────────────────── class TestIntentConfig: """IntentConfig 数据类测试""" def test_default_values(self): intent = IntentConfig() assert intent.keywords == [] assert intent.description == "" assert intent.examples == [] def test_from_dict_with_all_fields(self): data = { "keywords": ["生成", "写作"], "description": "内容生成意图", "examples": ["帮我写一篇文章", "生成一段文案"], } intent = IntentConfig(**data) assert intent.keywords == ["生成", "写作"] assert intent.description == "内容生成意图" assert intent.examples == ["帮我写一篇文章", "生成一段文案"] def test_empty_keywords_is_valid(self): intent = IntentConfig(keywords=[]) assert intent.keywords == [] # ── QualityGateConfig 测试 ───────────────────────────────── class TestQualityGateConfig: """QualityGateConfig 数据类测试""" def test_default_values(self): gate = QualityGateConfig() assert gate.required_fields == [] assert gate.min_word_count == 0 assert gate.max_retries == 0 assert gate.custom_validator is None def test_from_dict_with_all_fields(self): data = { "required_fields": ["title", "body"], "min_word_count": 100, "max_retries": 3, "custom_validator": "validators.check_quality", } gate = QualityGateConfig(**data) assert gate.required_fields == ["title", "body"] assert gate.min_word_count == 100 assert gate.max_retries == 3 assert gate.custom_validator == "validators.check_quality" def test_max_retries_defaults_to_zero(self): gate = QualityGateConfig() assert gate.max_retries == 0 # ── SkillConfig 测试 ─────────────────────────────────────── class TestSkillConfig: """SkillConfig 继承 AgentConfig 并扩展 v2 字段""" def test_from_dict_with_intent_and_quality_gate(self): data = { "name": "content_gen", "agent_type": "content_generation", "task_mode": "llm_generate", "prompt": {"identity": "你是内容生成助手"}, "intent": { "keywords": ["生成", "写作"], "description": "内容生成意图", "examples": ["帮我写文章"], }, "quality_gate": { "required_fields": ["title", "body"], "min_word_count": 100, "max_retries": 3, }, "execution_mode": "react", "max_steps": 10, } config = SkillConfig.from_dict(data) assert config.name == "content_gen" assert config.intent.keywords == ["生成", "写作"] assert config.intent.description == "内容生成意图" assert config.quality_gate.required_fields == ["title", "body"] assert config.quality_gate.max_retries == 3 assert config.execution_mode == "react" assert config.max_steps == 10 def test_from_old_agent_config_dict_auto_fills_defaults(self): """旧 AgentConfig 字典(无 intent/quality_gate)应自动填充默认值""" data = { "name": "geo_writer", "agent_type": "geo_writing", "task_mode": "llm_generate", "prompt": {"identity": "你是 GEO 写作助手"}, } config = SkillConfig.from_dict(data) assert config.name == "geo_writer" assert isinstance(config.intent, IntentConfig) assert config.intent.keywords == [] assert config.intent.description == "" assert config.intent.examples == [] assert isinstance(config.quality_gate, QualityGateConfig) assert config.quality_gate.required_fields == [] assert config.quality_gate.max_retries == 0 def test_execution_mode_defaults_to_react(self): data = { "name": "test_skill", "agent_type": "test", "task_mode": "llm_generate", "prompt": {"identity": "test"}, } config = SkillConfig.from_dict(data) assert config.execution_mode == "react" def test_max_steps_defaults_to_five(self): data = { "name": "test_skill", "agent_type": "test", "task_mode": "llm_generate", "prompt": {"identity": "test"}, } config = SkillConfig.from_dict(data) assert config.max_steps == 5 def test_backward_compat_old_yaml_without_intent(self): """旧 YAML 无 intent 字段 → intent 默认为空 IntentConfig""" yaml_content = yaml.dump({ "name": "legacy_skill", "agent_type": "legacy", "task_mode": "llm_generate", "prompt": {"identity": "旧技能"}, }) with tempfile.NamedTemporaryFile( mode="w", suffix=".yaml", delete=False, encoding="utf-8" ) as f: f.write(yaml_content) path = f.name try: config = SkillConfig.from_yaml(path) assert config.name == "legacy_skill" assert isinstance(config.intent, IntentConfig) assert config.intent.keywords == [] assert isinstance(config.quality_gate, QualityGateConfig) assert config.quality_gate.max_retries == 0 assert config.execution_mode == "react" finally: os.unlink(path) def test_from_yaml_loads_correctly(self): yaml_content = yaml.dump({ "name": "yaml_skill", "agent_type": "yaml_type", "task_mode": "llm_generate", "prompt": {"identity": "YAML 技能"}, "intent": {"keywords": ["yaml"], "description": "YAML 加载测试"}, "quality_gate": {"required_fields": ["result"], "max_retries": 2}, "execution_mode": "direct", "max_steps": 3, }) with tempfile.NamedTemporaryFile( mode="w", suffix=".yaml", delete=False, encoding="utf-8" ) as f: f.write(yaml_content) path = f.name try: config = SkillConfig.from_yaml(path) assert config.name == "yaml_skill" assert config.intent.keywords == ["yaml"] assert config.quality_gate.max_retries == 2 assert config.execution_mode == "direct" assert config.max_steps == 3 finally: os.unlink(path) def test_to_dict_includes_v2_fields(self): data = { "name": "dict_skill", "agent_type": "dict_type", "task_mode": "llm_generate", "prompt": {"identity": "字典技能"}, "intent": {"keywords": ["dict"]}, "quality_gate": {"required_fields": ["output"]}, "execution_mode": "custom", "max_steps": 7, } config = SkillConfig.from_dict(data) result = config.to_dict() assert "intent" in result assert result["intent"]["keywords"] == ["dict"] assert "quality_gate" in result assert result["quality_gate"]["required_fields"] == ["output"] assert result["execution_mode"] == "custom" assert result["max_steps"] == 7 def test_to_dict_includes_v2_defaults_when_not_provided(self): data = { "name": "minimal_skill", "agent_type": "minimal", "task_mode": "llm_generate", "prompt": {"identity": "最小技能"}, } config = SkillConfig.from_dict(data) result = config.to_dict() assert "intent" in result assert result["intent"]["keywords"] == [] assert "quality_gate" in result assert result["quality_gate"]["max_retries"] == 0 assert result["execution_mode"] == "react" assert result["max_steps"] == 5 def test_fallback_strategies_default_is_none(self): """SkillConfig.fallback_strategies defaults to None (#6).""" config = SkillConfig( name="test", agent_type="test", task_mode="llm_generate", prompt={"identity": "test"}, ) assert config.fallback_strategies is None def test_fallback_strategies_from_dict(self): """from_dict populates fallback_strategies (#6).""" data = { "name": "test", "agent_type": "test", "task_mode": "llm_generate", "prompt": {"identity": "test"}, "fallback_strategies": ["simplified_rewoo", "react", "direct"], } config = SkillConfig.from_dict(data) assert config.fallback_strategies == ["simplified_rewoo", "react", "direct"] def test_fallback_strategies_to_dict(self): """to_dict includes fallback_strategies when set (#6, #14).""" config = SkillConfig( name="test", agent_type="test", task_mode="llm_generate", prompt={"identity": "test"}, fallback_strategies=["simplified_rewoo", "direct"], ) result = config.to_dict() assert result["fallback_strategies"] == ["simplified_rewoo", "direct"] def test_fallback_strategies_to_dict_none_when_not_set(self): """to_dict includes fallback_strategies as None when not configured (#14).""" config = SkillConfig( name="test", agent_type="test", task_mode="llm_generate", prompt={"identity": "test"}, ) result = config.to_dict() assert "fallback_strategies" in result assert result["fallback_strategies"] is None def test_fallback_strategies_yaml_round_trip(self): """YAML round-trip preserves fallback_strategies (#6).""" import tempfile data = { "name": "round_trip", "agent_type": "test", "task_mode": "llm_generate", "prompt": {"identity": "test"}, "fallback_strategies": ["simplified_rewoo", "react", "direct"], } with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: yaml.dump(data, f) path = f.name try: config = SkillConfig.from_yaml(path) assert config.fallback_strategies == ["simplified_rewoo", "react", "direct"] # Round-trip through to_dict -> from_dict round_tripped = SkillConfig.from_dict(config.to_dict()) assert round_tripped.fallback_strategies == ["simplified_rewoo", "react", "direct"] finally: os.unlink(path) def test_fallback_strategies_invalid_value_raises_error(self): """Invalid fallback_strategies value raises ConfigValidationError (#20).""" # _validate_v2 is called in __init__, so construction itself raises with pytest.raises(ConfigValidationError, match="fallback_strategies"): SkillConfig( name="test_invalid_fallback", agent_type="test", task_mode="llm_generate", prompt={"identity": "test"}, fallback_strategies=["simplified_rewoo", "invalid_strategy"], ) def test_fallback_strategies_invalid_value_via_from_dict(self): """Invalid fallback_strategies via from_dict also raises (#20).""" with pytest.raises(ConfigValidationError, match="fallback_strategies"): SkillConfig.from_dict({ "name": "test_invalid_fallback", "agent_type": "test", "task_mode": "llm_generate", "prompt": {"identity": "test"}, "fallback_strategies": ["bogus"], }) def test_fallback_strategies_valid_subset_accepted(self): """Valid subset of ReWOOEngine.VALID_STRATEGIES is accepted (#20).""" # All valid strategies for strategies in [ ["simplified_rewoo"], ["react", "direct"], ["simplified_rewoo", "react", "direct", "plan_exec"], ]: config = SkillConfig( name="test", agent_type="test", task_mode="llm_generate", prompt={"identity": "test"}, fallback_strategies=strategies, ) assert config.fallback_strategies == strategies def test_fallback_strategies_none_bypasses_validation(self): """None fallback_strategies skips validation (uses ReWOOEngine defaults) (#20).""" config = SkillConfig( name="test", agent_type="test", task_mode="llm_generate", prompt={"identity": "test"}, fallback_strategies=None, ) assert config.fallback_strategies is None def test_invalid_execution_mode_raises_config_validation_error(self): data = { "name": "bad_mode", "agent_type": "bad", "task_mode": "llm_generate", "prompt": {"identity": "坏模式"}, "execution_mode": "invalid_mode", } with pytest.raises(ConfigValidationError): SkillConfig.from_dict(data) def test_direct_execution_mode(self): data = { "name": "direct_skill", "agent_type": "direct", "task_mode": "tool_call", "tools": ["some_tool"], "execution_mode": "direct", } config = SkillConfig.from_dict(data) assert config.execution_mode == "direct" def test_custom_execution_mode(self): data = { "name": "custom_skill", "agent_type": "custom", "task_mode": "custom", "custom_handler": "handlers.custom", "execution_mode": "custom", } config = SkillConfig.from_dict(data) assert config.execution_mode == "custom" # ── Skill 测试 ───────────────────────────────────────────── class TestSkill: """Skill 类测试""" def _make_config(self, name: str = "test_skill") -> SkillConfig: return SkillConfig.from_dict({ "name": name, "agent_type": "test", "task_mode": "llm_generate", "prompt": {"identity": "测试技能"}, }) def test_skill_name_property(self): config = self._make_config("my_skill") skill = Skill(config) assert skill.name == "my_skill" def test_skill_config_property(self): config = self._make_config() skill = Skill(config) assert skill.config is config def test_skill_tools_default_empty(self): config = self._make_config() skill = Skill(config) assert skill.tools == [] def test_skill_bind_tool(self): from agentkit.tools.base import Tool class DummyTool(Tool): async def execute(self, **kwargs): return {} config = self._make_config() skill = Skill(config) tool = DummyTool(name="t1", description="test tool") skill.bind_tool(tool) assert len(skill.tools) == 1 assert skill.tools[0].name == "t1" def test_skill_unbind_tool(self): from agentkit.tools.base import Tool class DummyTool(Tool): async def execute(self, **kwargs): return {} config = self._make_config() skill = Skill(config) tool = DummyTool(name="t1", description="test tool") skill.bind_tool(tool) skill.unbind_tool("t1") assert skill.tools == [] def test_skill_unbind_nonexistent_tool_no_error(self): config = self._make_config() skill = Skill(config) skill.unbind_tool("nonexistent") # 不应抛异常 assert skill.tools == [] def test_skill_to_dict(self): config = self._make_config() skill = Skill(config) d = skill.to_dict() assert "config" in d assert d["config"]["name"] == "test_skill" assert "tools" in d assert d["tools"] == [] def test_skill_with_tools_in_constructor(self): from agentkit.tools.base import Tool class DummyTool(Tool): async def execute(self, **kwargs): return {} config = self._make_config() tool = DummyTool(name="t1", description="test tool") skill = Skill(config, tools=[tool]) assert len(skill.tools) == 1