369 lines
14 KiB
Python
369 lines
14 KiB
Python
"""U11+U12 测试: Evolution 生命周期集成 + EvolutionConfig
|
|
|
|
覆盖:
|
|
- EvolutionConfig 默认值与自定义值
|
|
- SkillConfig 的 evolution 字段
|
|
- ConfigDrivenAgent 集成 EvolutionMixin
|
|
- 生命周期钩子触发进化
|
|
- 进化失败不影响主任务流程
|
|
"""
|
|
|
|
from datetime import datetime, timezone
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from agentkit.core.protocol import TaskMessage, TaskResult, TaskStatus
|
|
|
|
|
|
# ── Helpers ──────────────────────────────────────────────
|
|
|
|
|
|
def _make_task(**overrides) -> TaskMessage:
|
|
defaults = dict(
|
|
task_id="test-task-001",
|
|
agent_name="test_agent",
|
|
task_type="generate",
|
|
priority=1,
|
|
input_data={"query": "hello"},
|
|
callback_url=None,
|
|
created_at=datetime.now(timezone.utc),
|
|
)
|
|
defaults.update(overrides)
|
|
return TaskMessage(**defaults)
|
|
|
|
|
|
def _make_task_result(**overrides) -> TaskResult:
|
|
defaults = dict(
|
|
task_id="test-task-001",
|
|
agent_name="test_agent",
|
|
status=TaskStatus.COMPLETED,
|
|
output_data={"result": "ok"},
|
|
error_message=None,
|
|
started_at=datetime.now(timezone.utc),
|
|
completed_at=datetime.now(timezone.utc),
|
|
)
|
|
defaults.update(overrides)
|
|
return TaskResult(**defaults)
|
|
|
|
|
|
# ── EvolutionConfig 测试 ──────────────────────────────────
|
|
|
|
|
|
class TestEvolutionConfig:
|
|
"""U12: EvolutionConfig 数据类测试"""
|
|
|
|
def test_default_values(self):
|
|
"""默认 EvolutionConfig — enabled=False"""
|
|
from agentkit.skills.base import EvolutionConfig
|
|
|
|
config = EvolutionConfig()
|
|
assert config.enabled is False
|
|
assert config.reflect_on_failure is True
|
|
assert config.auto_apply is False
|
|
assert config.min_quality_threshold == 0.5
|
|
|
|
def test_from_dict_all_fields(self):
|
|
"""EvolutionConfig 从字典创建 — 所有字段设置"""
|
|
from agentkit.skills.base import EvolutionConfig
|
|
|
|
config = EvolutionConfig(
|
|
enabled=True,
|
|
reflect_on_failure=False,
|
|
auto_apply=True,
|
|
min_quality_threshold=0.8,
|
|
)
|
|
assert config.enabled is True
|
|
assert config.reflect_on_failure is False
|
|
assert config.auto_apply is True
|
|
assert config.min_quality_threshold == 0.8
|
|
|
|
def test_from_dict_partial(self):
|
|
"""EvolutionConfig 部分字段 — 缺失字段使用默认值"""
|
|
from agentkit.skills.base import EvolutionConfig
|
|
|
|
config = EvolutionConfig(enabled=True)
|
|
assert config.enabled is True
|
|
assert config.reflect_on_failure is True # default
|
|
assert config.auto_apply is False # default
|
|
assert config.min_quality_threshold == 0.5 # default
|
|
|
|
|
|
# ── SkillConfig evolution 字段测试 ─────────────────────────
|
|
|
|
|
|
class TestSkillConfigEvolution:
|
|
"""U12: SkillConfig 的 evolution 字段"""
|
|
|
|
def test_skill_config_without_evolution(self):
|
|
"""SkillConfig 无 evolution — 默认 enabled=False"""
|
|
from agentkit.skills.base import SkillConfig
|
|
|
|
config = SkillConfig(
|
|
name="test_agent",
|
|
agent_type="test",
|
|
task_mode="llm_generate",
|
|
prompt={"identity": "test", "instructions": "test"},
|
|
)
|
|
assert config.evolution.enabled is False
|
|
|
|
def test_skill_config_with_evolution(self):
|
|
"""SkillConfig 有 evolution 配置 — 正确解析"""
|
|
from agentkit.skills.base import SkillConfig
|
|
|
|
config = SkillConfig(
|
|
name="test_agent",
|
|
agent_type="test",
|
|
task_mode="llm_generate",
|
|
prompt={"identity": "test", "instructions": "test"},
|
|
evolution={"enabled": True, "auto_apply": True, "min_quality_threshold": 0.7},
|
|
)
|
|
assert config.evolution.enabled is True
|
|
assert config.evolution.auto_apply is True
|
|
assert config.evolution.min_quality_threshold == 0.7
|
|
|
|
def test_skill_config_to_dict_includes_evolution(self):
|
|
"""SkillConfig.to_dict 包含 evolution 字段"""
|
|
from agentkit.skills.base import SkillConfig
|
|
|
|
config = SkillConfig(
|
|
name="test_agent",
|
|
agent_type="test",
|
|
task_mode="llm_generate",
|
|
prompt={"identity": "test", "instructions": "test"},
|
|
evolution={"enabled": True},
|
|
)
|
|
d = config.to_dict()
|
|
assert "evolution" in d
|
|
assert d["evolution"]["enabled"] is True
|
|
assert d["evolution"]["reflect_on_failure"] is True
|
|
assert d["evolution"]["auto_apply"] is False
|
|
assert d["evolution"]["min_quality_threshold"] == 0.5
|
|
|
|
def test_skill_config_from_dict_with_evolution(self):
|
|
"""SkillConfig.from_dict 正确解析 evolution"""
|
|
from agentkit.skills.base import SkillConfig
|
|
|
|
data = {
|
|
"name": "test_agent",
|
|
"agent_type": "test",
|
|
"task_mode": "llm_generate",
|
|
"prompt": {"identity": "test", "instructions": "test"},
|
|
"evolution": {"enabled": True, "reflect_on_failure": False},
|
|
}
|
|
config = SkillConfig.from_dict(data)
|
|
assert config.evolution.enabled is True
|
|
assert config.evolution.reflect_on_failure is False
|
|
|
|
|
|
# ── ConfigDrivenAgent evolution 集成测试 ──────────────────
|
|
|
|
|
|
class TestConfigDrivenAgentEvolution:
|
|
"""U11: ConfigDrivenAgent 集成 EvolutionMixin"""
|
|
|
|
def _make_agent_config(self, evolution=None):
|
|
from agentkit.core.config_driven import AgentConfig
|
|
|
|
config = AgentConfig(
|
|
name="test_agent",
|
|
agent_type="test",
|
|
task_mode="llm_generate",
|
|
prompt={"identity": "test", "instructions": "test"},
|
|
)
|
|
if evolution is not None:
|
|
config.evolution = evolution
|
|
return config
|
|
|
|
def _make_skill_config(self, evolution=None):
|
|
from agentkit.skills.base import SkillConfig
|
|
|
|
return SkillConfig(
|
|
name="test_agent",
|
|
agent_type="test",
|
|
task_mode="llm_generate",
|
|
prompt={"identity": "test", "instructions": "test"},
|
|
evolution=evolution,
|
|
)
|
|
|
|
def test_agent_without_evolution_config(self):
|
|
"""Agent 无 evolution 配置 — _evolution_enabled=False"""
|
|
from agentkit.core.config_driven import ConfigDrivenAgent
|
|
|
|
config = self._make_agent_config()
|
|
agent = ConfigDrivenAgent(config=config)
|
|
assert agent._evolution_enabled is False
|
|
|
|
def test_agent_with_evolution_enabled(self):
|
|
"""Agent 有 evolution 且 enabled=True — _evolution_enabled=True"""
|
|
from agentkit.core.config_driven import ConfigDrivenAgent
|
|
|
|
config = self._make_agent_config(evolution={"enabled": True})
|
|
agent = ConfigDrivenAgent(config=config)
|
|
assert agent._evolution_enabled is True
|
|
|
|
def test_agent_with_evolution_disabled(self):
|
|
"""Agent 有 evolution 但 enabled=False — _evolution_enabled=False"""
|
|
from agentkit.core.config_driven import ConfigDrivenAgent
|
|
|
|
config = self._make_agent_config(evolution={"enabled": False})
|
|
agent = ConfigDrivenAgent(config=config)
|
|
assert agent._evolution_enabled is False
|
|
|
|
async def test_on_task_complete_evolution_disabled(self):
|
|
"""on_task_complete 进化禁用 — 不调用 evolve_after_task"""
|
|
from agentkit.core.config_driven import ConfigDrivenAgent
|
|
|
|
config = self._make_agent_config()
|
|
agent = ConfigDrivenAgent(config=config)
|
|
|
|
task = _make_task()
|
|
output = {"result": "ok"}
|
|
|
|
# Should not raise and should not call evolve_after_task
|
|
await agent.on_task_complete(task, output)
|
|
|
|
async def test_on_task_complete_evolution_enabled(self):
|
|
"""on_task_complete 进化启用 — 调用 evolve_after_task"""
|
|
from agentkit.core.config_driven import ConfigDrivenAgent
|
|
|
|
config = self._make_agent_config(evolution={"enabled": True})
|
|
agent = ConfigDrivenAgent(config=config)
|
|
|
|
task = _make_task()
|
|
output = {"result": "ok"}
|
|
|
|
with patch.object(agent, "evolve_after_task", new_callable=AsyncMock) as mock_evolve:
|
|
await agent.on_task_complete(task, output)
|
|
mock_evolve.assert_called_once()
|
|
# Verify the TaskResult passed to evolve_after_task
|
|
call_args = mock_evolve.call_args
|
|
result_arg = call_args[0][1] # second positional arg is TaskResult
|
|
assert result_arg.status == TaskStatus.COMPLETED
|
|
assert result_arg.output_data == output
|
|
|
|
async def test_on_task_failed_evolution_enabled(self):
|
|
"""on_task_failed 进化启用 — 调用 evolve_after_task"""
|
|
from agentkit.core.config_driven import ConfigDrivenAgent
|
|
|
|
config = self._make_agent_config(evolution={"enabled": True})
|
|
agent = ConfigDrivenAgent(config=config)
|
|
|
|
task = _make_task()
|
|
error = ValueError("test error")
|
|
|
|
with patch.object(agent, "evolve_after_task", new_callable=AsyncMock) as mock_evolve:
|
|
await agent.on_task_failed(task, error)
|
|
mock_evolve.assert_called_once()
|
|
# Verify the TaskResult passed to evolve_after_task
|
|
call_args = mock_evolve.call_args
|
|
result_arg = call_args[0][1] # second positional arg is TaskResult
|
|
assert result_arg.status == TaskStatus.FAILED
|
|
assert result_arg.error_message == "test error"
|
|
|
|
async def test_evolution_failure_does_not_break_task(self):
|
|
"""进化失败不影响任务完成"""
|
|
from agentkit.core.config_driven import ConfigDrivenAgent
|
|
|
|
config = self._make_agent_config(evolution={"enabled": True})
|
|
agent = ConfigDrivenAgent(config=config)
|
|
|
|
task = _make_task()
|
|
output = {"result": "ok"}
|
|
|
|
with patch.object(agent, "evolve_after_task", new_callable=AsyncMock, side_effect=RuntimeError("evolution crashed")):
|
|
# Should NOT raise — evolution failure is caught
|
|
await agent.on_task_complete(task, output)
|
|
|
|
async def test_evolution_failure_on_task_failed_does_not_break(self):
|
|
"""进化失败不影响 on_task_failed"""
|
|
from agentkit.core.config_driven import ConfigDrivenAgent
|
|
|
|
config = self._make_agent_config(evolution={"enabled": True})
|
|
agent = ConfigDrivenAgent(config=config)
|
|
|
|
task = _make_task()
|
|
error = ValueError("task error")
|
|
|
|
with patch.object(agent, "evolve_after_task", new_callable=AsyncMock, side_effect=RuntimeError("evolution crashed")):
|
|
# Should NOT raise
|
|
await agent.on_task_failed(task, error)
|
|
|
|
def test_skill_config_evolution_propagated(self):
|
|
"""SkillConfig 的 evolution 配置传递到 ConfigDrivenAgent"""
|
|
from agentkit.core.config_driven import ConfigDrivenAgent
|
|
|
|
config = self._make_skill_config(evolution={"enabled": True})
|
|
agent = ConfigDrivenAgent(config=config)
|
|
assert agent._evolution_enabled is True
|
|
|
|
|
|
# ── EvolutionMixin 集成测试 ───────────────────────────────
|
|
|
|
|
|
class TestEvolutionMixinIntegration:
|
|
"""U11: EvolutionMixin 方法集成到 ConfigDrivenAgent"""
|
|
|
|
def _make_agent_with_evolution(self):
|
|
from agentkit.core.config_driven import AgentConfig, ConfigDrivenAgent
|
|
|
|
config = AgentConfig(
|
|
name="test_agent",
|
|
agent_type="test",
|
|
task_mode="llm_generate",
|
|
prompt={"identity": "test", "instructions": "test"},
|
|
)
|
|
config.evolution = {"enabled": True}
|
|
return ConfigDrivenAgent(config=config)
|
|
|
|
def test_agent_has_get_evolution_history(self):
|
|
"""Agent 继承 get_evolution_history 方法"""
|
|
from agentkit.core.config_driven import ConfigDrivenAgent
|
|
from agentkit.evolution.lifecycle import EvolutionMixin
|
|
|
|
agent = self._make_agent_with_evolution()
|
|
assert hasattr(agent, "get_evolution_history")
|
|
assert callable(agent.get_evolution_history)
|
|
|
|
def test_agent_has_set_current_module(self):
|
|
"""Agent 继承 set_current_module 方法"""
|
|
from agentkit.core.config_driven import ConfigDrivenAgent
|
|
from agentkit.evolution.lifecycle import EvolutionMixin
|
|
|
|
agent = self._make_agent_with_evolution()
|
|
assert hasattr(agent, "set_current_module")
|
|
assert callable(agent.set_current_module)
|
|
|
|
def test_get_evolution_history_empty_initially(self):
|
|
"""get_evolution_history 初始返回空列表"""
|
|
agent = self._make_agent_with_evolution()
|
|
history = agent.get_evolution_history()
|
|
assert history == []
|
|
|
|
def test_set_current_module_works(self):
|
|
"""set_current_module 正常工作"""
|
|
from agentkit.evolution.prompt_optimizer import Module, Signature
|
|
|
|
agent = self._make_agent_with_evolution()
|
|
signature = Signature(
|
|
input_fields={"query": "user query"},
|
|
output_fields={"result": "result"},
|
|
instruction="test instructions",
|
|
)
|
|
module = Module(name="test_module", signature=signature)
|
|
agent.set_current_module(module)
|
|
assert agent._current_module is not None
|
|
assert agent._current_module.name == "test_module"
|
|
|
|
def test_mro_correct(self):
|
|
"""MRO 正确: ConfigDrivenAgent → BaseAgent → EvolutionMixin"""
|
|
from agentkit.core.config_driven import ConfigDrivenAgent
|
|
from agentkit.core.base import BaseAgent
|
|
from agentkit.evolution.lifecycle import EvolutionMixin
|
|
|
|
mro = ConfigDrivenAgent.__mro__
|
|
# BaseAgent should come before EvolutionMixin in MRO
|
|
base_idx = mro.index(BaseAgent)
|
|
mixin_idx = mro.index(EvolutionMixin)
|
|
assert base_idx < mixin_idx, f"BaseAgent (idx={base_idx}) should come before EvolutionMixin (idx={mixin_idx}) in MRO"
|