auto-sync: 2026-06-05 11:03:30

This commit is contained in:
cfdaily
2026-06-05 11:03:30 +08:00
parent e9c9aaddfe
commit 6a649aba07
30 changed files with 602 additions and 1276 deletions
+256
View File
@@ -0,0 +1,256 @@
"""F15 Experience Distillation 单元测试
按 test-plan-v2.6.md §F15
- T1: 经验提取(P0
- T2: 持久化(P0
- T3: 相似推荐(P0
- T4: 模式分类(P1
"""
import json
import pytest
pytestmark = pytest.mark.unit
from pathlib import Path
from src.daemon.experience import (
Experience,
ExperienceCategory,
ExperienceDistiller,
ExperienceStore,
)
# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
@pytest.fixture
def store(tmp_path):
return ExperienceStore(store_path=tmp_path / "experiences.jsonl")
@pytest.fixture
def distiller(store):
return ExperienceDistiller(store=store)
@pytest.fixture
def memory_store():
"""纯内存 store"""
return ExperienceStore()
# ---------------------------------------------------------------------------
# T1: 经验提取
# ---------------------------------------------------------------------------
class TestDistillation:
def test_distill_from_review_failure(self, distiller):
exps = distiller.distill_from_task(
task_id="t1",
task_title="Build Feature",
task_type="coding",
review_result={
"verdict": "fail",
"results": [
{"step": "existence", "verdict": "fail",
"details": "Missing output.md", "suggestions": []},
],
},
)
assert len(exps) >= 1
assert any(e.category == "pitfall" for e in exps)
def test_distill_from_suggestions(self, distiller):
exps = distiller.distill_from_task(
task_id="t2",
task_title="Write Tests",
task_type="testing",
review_result={
"verdict": "pass",
"results": [
{"step": "quality", "verdict": "pass", "score": 0.9,
"suggestions": ["Always test edge cases"]},
],
},
)
assert len(exps) >= 1
summaries = [e.summary for e in exps]
assert any("edge cases" in s for s in summaries)
def test_distill_from_text_output(self, distiller):
exps = distiller.distill_from_task(
task_id="t3",
task_title="Deploy Service",
outputs=[
{"content": "## Best Practice\n\nAlways use health checks when deploying services."},
],
)
assert len(exps) >= 1
assert any(e.category == "best_practice" for e in exps)
def test_distill_pitfall_from_text(self, distiller):
exps = distiller.distill_from_task(
task_id="t4",
task_title="Debug Issue",
outputs=[
{"content": "## Bug Report\n\nForgot to close the database connection."},
],
)
assert any(e.category == "pitfall" for e in exps)
def test_distill_environment_from_text(self, distiller):
exps = distiller.distill_from_task(
task_id="t5",
task_title="Setup",
outputs=[
{"content": "Need to install Python 3.9+ and configure the PATH."},
],
)
assert any(e.category == "environment" for e in exps)
def test_empty_outputs_no_crash(self, distiller):
exps = distiller.distill_from_task(
task_id="t6",
task_title="Empty Task",
)
assert exps == []
# ---------------------------------------------------------------------------
# T2: 持久化
# ---------------------------------------------------------------------------
class TestPersistence:
def test_save_and_reload(self, tmp_path):
path = tmp_path / "experiences.jsonl"
store1 = ExperienceStore(store_path=path)
exp = Experience(category="pitfall", summary="Test experience", tags=["test"])
store1.add(exp)
# Reload
store2 = ExperienceStore(store_path=path)
assert store2.count() == 1
loaded = store2.get(exp.id)
assert loaded is not None
assert loaded.summary == "Test experience"
def test_delete_persists(self, tmp_path):
path = tmp_path / "experiences.jsonl"
store = ExperienceStore(store_path=path)
exp = Experience(category="pitfall", summary="To delete")
store.add(exp)
store.delete(exp.id)
assert store.count() == 0
# Reload
store2 = ExperienceStore(store_path=path)
assert store2.count() == 0
def test_multiple_experiences(self, tmp_path):
path = tmp_path / "experiences.jsonl"
store = ExperienceStore(store_path=path)
for i in range(5):
store.add(Experience(category="pattern", summary=f"Exp {i}"))
assert store.count() == 5
store2 = ExperienceStore(store_path=path)
assert store2.count() == 5
def test_memory_store_no_file(self):
store = ExperienceStore()
store.add(Experience(category="test", summary="Memory only"))
assert store.count() == 1
# ---------------------------------------------------------------------------
# T3: 相似推荐
# ---------------------------------------------------------------------------
class TestRecommendation:
def test_recommend_by_tags(self, distiller, store):
store.add(Experience(category="pitfall", summary="Coding pitfall",
tags=["coding"]))
store.add(Experience(category="best_practice", summary="Testing BP",
tags=["testing"]))
results = distiller.recommend(tags=["coding"])
assert len(results) >= 1
assert any("Coding pitfall" in e.summary for e in results)
def test_recommend_by_query(self, distiller, store):
store.add(Experience(category="pitfall", summary="Always close DB connections"))
store.add(Experience(category="best_practice", summary="Use type hints"))
results = distiller.recommend(query="db")
assert len(results) >= 1
assert any("DB" in e.summary for e in results)
def test_recommend_by_task_type(self, distiller, store):
store.add(Experience(category="pitfall", summary="P1", tags=["coding"]))
store.add(Experience(category="pitfall", summary="P2", tags=["testing"]))
results = distiller.recommend(task_type="coding")
assert any("P1" in e.summary for e in results)
def test_recommend_empty(self, distiller):
results = distiller.recommend()
assert results == []
def test_recommend_limit(self, distiller, store):
for i in range(10):
store.add(Experience(category="pitfall", summary=f"Exp {i}",
tags=["coding"]))
results = distiller.recommend(tags=["coding"], limit=3)
assert len(results) <= 3
# ---------------------------------------------------------------------------
# T4: 模式分类
# ---------------------------------------------------------------------------
class TestPatternClassification:
def test_classify_pitfall(self, distiller):
assert distiller._classify_text("This is a common bug") == "pitfall"
def test_classify_best_practice(self, distiller):
assert distiller._classify_text("Always use version control") == "best_practice"
def test_classify_environment(self, distiller):
assert distiller._classify_text("Install the required packages") == "environment"
def test_classify_no_match(self, distiller):
assert distiller._classify_text("The weather is nice today") is None
def test_classify_chinese(self, distiller):
assert distiller._classify_text("这是一个常见的陷阱") == "pitfall"
assert distiller._classify_text("建议使用类型注解") == "best_practice"
# ---------------------------------------------------------------------------
# Experience model
# ---------------------------------------------------------------------------
class TestExperienceModel:
def test_to_dict_roundtrip(self):
exp = Experience(
category="pitfall",
summary="Test",
source_task_id="t1",
tags=["coding"],
)
d = exp.to_dict()
exp2 = Experience.from_dict(d)
assert exp2.summary == exp.summary
assert exp2.category == exp.category
assert exp2.tags == exp.tags
def test_search_by_category(self, store):
store.add(Experience(category="pitfall", summary="P1"))
store.add(Experience(category="best_practice", summary="B1"))
results = store.search(category="pitfall")
assert len(results) == 1
assert results[0].summary == "P1"