feat(evolution): evolve.py per-target evolution — skill/system/tool (#9)

This commit is contained in:
2026-07-07 10:26:58 -04:00
parent 6072ee7d0b
commit 4634414606
2 changed files with 924 additions and 30 deletions
+120 -2
View File
@@ -6,8 +6,9 @@
from __future__ import annotations
import asyncio
import json
from unittest.mock import AsyncMock
from unittest.mock import AsyncMock, MagicMock
import pytest
@@ -27,7 +28,11 @@ from core.evolution.evolve import (
_strip_protected_regions,
_system_protected_spans,
_tool_protected_spans,
consolidate_appendix,
edit_budget_at,
evolve_single_skill,
evolve_single_tool,
evolve_system_prompt,
rank_and_clip,
resolve_skill_file,
validate_skill,
@@ -40,7 +45,14 @@ from core.evolution.patch import (
MOMENTUM_END,
MOMENTUM_START,
)
from core.evolution.types import RejectedEdit
from core.evolution.types import (
EvolvePrompts,
RejectedEdit,
SkillCasePack,
SystemCasePack,
ToolCasePack,
)
from core.types import LLMResponse
# =========================================================================
# A. 内部辅助函数
@@ -657,3 +669,109 @@ class TestFormatRejectedEdits:
assert "skill.md" in result
assert "changed X" in result
assert "W=" not in result
# =========================================================================
# I. 进化入口函数测试(Task 8)
# =========================================================================
_PROMPTS = EvolvePrompts(
evolve_skill="sk",
evolve_system="sys",
evolve_tool="tool",
evolve_rank="rank",
consolidate_system="cons",
)
def _make_fake_llm(response_content: str) -> AsyncMock:
"""构造返回固定 LLMResponse 的模拟 LLM。"""
mock = AsyncMock()
mock.chat.return_value = LLMResponse(
content=response_content,
thinking="",
model="test",
provider="test",
prompt_tokens=0,
completion_tokens=0,
latency_ms=0,
ttft_ms=None,
max_inter_token_ms=None,
cache_hit=False,
call_id="test-id",
)
return mock
class TestEvolveSingleSkill:
"""evolve_single_skill 测试。"""
def test_empty_pack_skipped(self) -> None:
"""空案例包(无失败、无 lapse)导致无 applied edits → rejected。"""
pack = SkillCasePack(
task_type="test",
target_file="test.md",
stats={},
failure_cases=[],
success_cases=[],
lapse_notes=[],
)
store = MagicMock()
store.read_skill.return_value = "---\nname: t\ndescription: d\ntask_type: t\n---\nbody"
store.list_skill_files.return_value = ["test.md"]
llm = _make_fake_llm('{"suggestions":[],"edits":[]}')
record = asyncio.run(evolve_single_skill(llm, pack, store, _PROMPTS, "v1", 5, 6))
assert record.status in ("rejected", "skipped")
class TestEvolveSystemPrompt:
"""evolve_system_prompt 测试。"""
def test_no_failures_returns_skipped(self) -> None:
"""空 failure_cases + 空 edits → 无 applied → rejected。"""
pack = SystemCasePack(stats={}, failure_cases=[], success_cases=[])
store = MagicMock()
store.read_prompt.return_value = (
"## 能力边界\nfixed\n## 输出格式\nfixed\n## 视频树结构\nfixed\nbody"
)
llm = _make_fake_llm('{"suggestions":[],"edits":[]}')
record = asyncio.run(evolve_system_prompt(llm, pack, store, _PROMPTS, "v1", 5))
assert record.status in ("rejected", "skipped")
class TestEvolveSingleTool:
"""evolve_single_tool 测试。"""
def test_evolved_content_is_json(self) -> None:
"""即使 rejectedevolved_content 仍是合法 JSON 含 extract/verify。"""
pack = ToolCasePack(
tool_name="view_node",
target_files=["view_node_extract.md", "view_node_verify.md"],
stats={},
failure_spans=[],
success_spans=[],
)
store = MagicMock()
store.read_prompt.return_value = "## 输出格式\nfixed\nbody"
llm = _make_fake_llm('{"suggestions":[],"edits":[]}')
record = asyncio.run(evolve_single_tool(llm, pack, store, _PROMPTS, "v1", 5))
parsed = json.loads(record.evolved_content)
assert "extract" in parsed and "verify" in parsed
class TestConsolidateAppendix:
"""consolidate_appendix 测试。"""
def test_single_note_passthrough(self) -> None:
"""G1 守卫:单条 note 直接返回,不调 LLM。"""
llm = _make_fake_llm("")
result = asyncio.run(consolidate_appendix(llm, ["note1"]))
assert result == ["note1"]
def test_exception_returns_original(self) -> None:
"""G3 守卫:LLM 异常时降级返回原 notes。"""
llm = AsyncMock()
llm.chat.side_effect = RuntimeError("boom")
result = asyncio.run(consolidate_appendix(llm, ["a", "b", "c"]))
assert result == ["a", "b", "c"]