"""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"