# ============================================================ # 偏好学习升级黄金测试(plan.md §8.3 / 矩阵 66 行) # 覆盖:项目隔离、解释(得分明细+冷启动/不确定度)、重置、向后兼容。 # ============================================================ from __future__ import annotations from pathlib import Path from server.knowledge.preferences import PreferenceStore def _store(tmp_path: Path) -> PreferenceStore: return PreferenceStore(path=str(tmp_path / "pref.json")) def test_preference_project_isolation(tmp_path: Path): """项目隔离:不同项目各算各的分,个人全局不受项目样本影响。""" ps = _store(tmp_path) ps.record("DELIVERY_FIRST", source="schedule.run", actor="u1", project_id="p1") ps.record("DELIVERY_FIRST", source="schedule.run", actor="u1", project_id="p1") ps.record("CAPACITY_BALANCE", source="scenario.apply", actor="u1", project_id="p2") ps.record("CAPACITY_BALANCE", source="scenario.apply", actor="u1", project_id="p2") d1, s1 = ps.preferred_strategy(project_id="p1") d2, s2 = ps.preferred_strategy(project_id="p2") assert d1 == "DELIVERY_FIRST" and s1["DELIVERY_FIRST"] == 2 assert d2 == "CAPACITY_BALANCE" and s2["CAPACITY_BALANCE"] == 4 d0, s0 = ps.preferred_strategy() # 个人全局(无项目) assert d0 == "COMPREHENSIVE" and s0 == {} def test_preference_explain_cold_and_high(tmp_path: Path): """解释:无样本 → cold;样本充足 → high 且带得分明细与依据条数。""" ps = _store(tmp_path) cold = ps.explain() assert cold["confidence"] == "cold" assert cold["sampleCount"] == 0 for _ in range(4): ps.record("DELIVERY_FIRST", source="scenario.apply", actor="u1", project_id="p1") warm = ps.explain(project_id="p1") assert warm["confidence"] == "high" assert warm["strategy"] == "DELIVERY_FIRST" assert warm["sampleCount"] == 4 assert warm["scores"]["DELIVERY_FIRST"] == 8 def test_preference_explain_tie_is_medium(tmp_path: Path): """不确定度:并列最高分 → medium(偏好尚不明确)。""" ps = _store(tmp_path) ps.record("DELIVERY_FIRST", source="scenario.apply", actor="u1", project_id="p1") ps.record("CAPACITY_BALANCE", source="scenario.apply", actor="u1", project_id="p1") ex = ps.explain(project_id="p1") assert ex["confidence"] == "medium" def test_preference_reset_by_project(tmp_path: Path): """重置:只清指定项目样本,其他项目与个人全局保留。""" ps = _store(tmp_path) ps.record("DELIVERY_FIRST", source="schedule.run", actor="u1", project_id="p1") ps.record("CAPACITY_BALANCE", source="schedule.run", actor="u1", project_id="p2") ps.record("FIFO", source="schedule.run", actor="u1") removed = ps.reset(project_id="p1") assert removed == 1 assert len(ps._samples_for("p1")) == 0 assert len(ps._samples_for("p2")) == 1 assert len(ps._samples_for(None)) == 1 # 个人全局保留