"""评测指标纯函数单元测试(不依赖 Qdrant / Ollama)""" import pytest from scripts.eval.metrics import ( aggregate, entity_recall, precision_at_k, pruning_loss, recall_at_k, routing_f1, ) class TestEntityRecall: def test_all_entities_kept(self) -> None: source = "使用 Qdrant 存储 1536 维向量,模型 bge-m3,见《部署手册》第三条。" summary = "基于 Qdrant 与 bge-m3 的 1536 维向量检索,详见《部署手册》第三条。" assert entity_recall(source, summary) == pytest.approx(1.0) def test_partial_entities_kept(self) -> None: source = "支持 text-embedding-3-small 和 bge-m3 两个模型。" summary = "支持 bge-m3 模型。" score = entity_recall(source, summary) assert 0.0 < score < 1.0 def test_no_entities_returns_full_score(self) -> None: assert entity_recall("这是一段没有任何实体的中文普通句子。", "很短") == 1.0 def test_empty_summary_drops_all(self) -> None: source = "版本 v1.2.0 修复了 3 个问题。" assert entity_recall(source, "") == pytest.approx(0.0) class TestRoutingF1: def test_perfect_routing(self) -> None: golden = [{"d1"}, {"d2"}] routed = [{"d1"}, {"d2"}] result = routing_f1(golden, routed) assert result == {"precision": 1.0, "recall": 1.0, "f1": 1.0} def test_micro_average(self) -> None: # query1: tp=1 fp=1;query2: tp=0 fn=1 → micro p=1/2 r=1/2 f1=1/2 golden = [{"d1"}, {"d2"}] routed = [{"d1", "d3"}, set()] result = routing_f1(golden, routed) assert result["precision"] == pytest.approx(0.5) assert result["recall"] == pytest.approx(0.5) assert result["f1"] == pytest.approx(0.5) def test_empty_routed(self) -> None: result = routing_f1([{"d1"}], [set()]) assert result["precision"] == 0.0 assert result["recall"] == 0.0 class TestPruningLoss: def test_mixed_cases(self) -> None: assert pruning_loss([False, True, False, False]) == pytest.approx(0.25) def test_empty(self) -> None: assert pruning_loss([]) == 0.0 class TestPrecisionRecallAtK: def test_precision_at_k(self) -> None: hits = ["d1", "d2", "d3", "d4", "d5"] assert precision_at_k(hits, {"d1", "d3"}, 5) == pytest.approx(0.4) def test_precision_at_k_empty_hits(self) -> None: assert precision_at_k([], {"d1"}, 5) == 0.0 def test_recall_at_k_dedup(self) -> None: hits = ["d1", "d1", "d2"] assert recall_at_k(hits, {"d1", "d2", "d3"}, 3) == pytest.approx(2 / 3) def test_recall_at_k_empty_golden(self) -> None: assert recall_at_k(["d1"], set(), 5) == 0.0 class TestAggregate: def test_mean_per_key(self) -> None: records = [{"a": 1.0, "b": 0.5}, {"a": 0.0, "b": 1.0}] result = aggregate(records) assert result["a"] == pytest.approx(0.5) assert result["b"] == pytest.approx(0.75) def test_partial_keys(self) -> None: result = aggregate([{"a": 1.0}, {"a": 0.0, "b": 1.0}]) assert result["a"] == pytest.approx(0.5) assert result["b"] == pytest.approx(1.0) def test_empty(self) -> None: assert aggregate([]) == {}