"""An evaluator must reject wrong evidence and outages, not just print scores.""" from __future__ import annotations import sys import unittest import json import tempfile import contextlib import io from pathlib import Path from unittest import mock sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts")) import xkb_eval class RecallEvaluation(unittest.TestCase): def packet(self, ids): return {"records": [{"id": x} for x in ids], "count": len(ids), "retrieval_mode": "keyword_fallback", "judge": {"status": "unavailable"}} def test_wrong_evidence_lowers_precision_and_fails(self): result = xkb_eval.score_case({"id": "q", "expected_ids": ["good"]}, self.packet(["good", "noise"])) self.assertFalse(result["ok"]) self.assertEqual(result["recall_at_k"], 1) self.assertEqual(result["precision_at_k"], .5) def test_missing_half_of_conflicting_evidence_fails(self): result = xkb_eval.score_case({"id": "q", "expected_ids": ["for", "against"]}, self.packet(["for"])) self.assertFalse(result["ok"]) self.assertEqual(result["recall_at_k"], .5) def test_no_answer_query_rejects_any_result(self): case = {"id": "q", "expected_ids": []} self.assertFalse(xkb_eval.score_case(case, self.packet(["noise"]))["ok"]) self.assertTrue(xkb_eval.score_case(case, self.packet([]))["ok"]) def test_conversational_need_accepts_equivalent_evidence_but_not_unrelated_hits(self): case = {"id": "statement", "expected_ids": [], "expected_any_ids": ["card", "wiki"], "expected_delivery": "evidence", "need": "Resolve the current obstacle"} hit = xkb_eval.score_case(case, self.packet(["wiki"])) self.assertTrue(hit["ok"]) self.assertFalse(hit["no_answer"]) self.assertEqual(hit["recall_at_k"], 1) miss = xkb_eval.score_case(case, self.packet(["unrelated"])) self.assertFalse(miss["ok"]) self.assertEqual(miss["recall_at_k"], 0) def test_distinct_sections_are_not_duplicates_and_document_precision_is_consistent(self): packet = self.packet(["guide", "guide", "noise"]) for record, section in zip(packet["records"], ["setup", "recovery", ""]): record.update(record_type="wiki_topic", section=section) result = xkb_eval.score_case({"id": "q", "expected_ids": ["guide"]}, packet) self.assertNotIn("duplicate evidence", result["errors"]) self.assertEqual(result["precision_at_k"], .5) packet["records"].pop() self.assertTrue(xkb_eval.score_case({"id": "q", "expected_ids": ["guide"]}, packet)["ok"]) def test_evidence_labels_detect_missing_section_and_namespace(self): records = [{"id": "guide", "record_type": "wiki_topic", "section": section, "namespace": namespace} for section, namespace in [("setup", "private"), ("recovery", "private"), ("setup", "other")]] keys = [xkb_eval.identity_key(r) for r in records] case = {"id": "q", "label_unit": "evidence", "expected_ids": keys[:2]} packet = self.packet([]) packet["records"] = records[::2] result = xkb_eval.score_case(case, packet) self.assertFalse(result["ok"]) self.assertEqual(result["recall_at_k"], .5) self.assertEqual(result["precision_at_k"], .5) self.assertIn("missing: " + keys[1], result["errors"]) def test_same_evidence_from_two_retrieval_legs_is_a_duplicate(self): packet = self.packet(["cards/guide.md", "guide"]) self.assertIn("duplicate evidence", xkb_eval.score_case( {"id": "q", "expected_ids": [], "allowed_ids": ["cards/guide.md", "guide"]}, packet)["errors"]) def test_anonymous_records_fail_as_unidentified_not_duplicates(self): result = xkb_eval.score_case({"id": "q", "expected_ids": []}, self.packet(["", ""])) self.assertIn("unidentified evidence", result["errors"]) self.assertNotIn("duplicate evidence", result["errors"]) def test_transport_failure_is_not_dropped_from_recall_denominator(self): cases = [{"id": "positive", "query": "q", "expected_ids": ["good"]}, {"id": "negative", "query": "q", "expected_ids": []}] with mock.patch.object(xkb_eval, "probe", side_effect=RuntimeError("offline")): result = xkb_eval.run_cases(cases, env={}) self.assertFalse(result["ok"]) self.assertEqual(result["mean_recall_at_k"], 0) self.assertEqual(result["no_answer_cases"], 1) self.assertEqual(result["no_answer_passed"], 0) def test_delivery_evaluation_rejects_overdelivery_and_unknown_judgement(self): packet = self.packet(['a','b']) packet['delivery'] = {'records': packet['records'], 'status': 'degraded'} row = xkb_eval.score_case({'id': 'q', 'surface': 'delivery', 'expected_ids': ['a','b'], 'max_delivered': 1}, packet) self.assertIn('delivery budget exceeded', row['errors']) self.assertIn('proactive delivery judgement incomplete', row['errors']) if __name__ == "__main__": unittest.main()