feat(evolution): evolve.py per-target evolution — skill/system/tool (#9)
This commit is contained in:
+120
-2
@@ -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:
|
||||
"""即使 rejected,evolved_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"]
|
||||
|
||||
Reference in New Issue
Block a user