AgentEvalTool/tests/unit/test_case_verdict.py
sinohqb 2fddce8c92 refactor(case-verdict): extract build_case_evidence as single evidence-construction seam
用例判定证据构建收敛到 case_verdict.py 的 build_case_evidence 纯函数,
report.py 和 runs.py 各删 ~15 行重复逻辑,换一行调用。locality 回归:
证据构建改一处,全局生效。

- 新增 build_case_evidence(turns, results) -> dict[str, CaseEvidence]
- report.py:76-83 证据构建替换为一行调用
- runs.py:175-201 证据构建替换为一行调用
- 5 个新测试覆盖 build_case_evidence(纯函数,无 DB 依赖)
2026-08-04 11:23:56 +08:00

173 lines
6.9 KiB
Python

"""Unit tests for resolve_case_verdicts — the single case-verdict read seam."""
from agenteval.evaluation.case_verdict import CaseEvidence, build_case_evidence, resolve_case_verdicts
from agenteval.models import CaseOutcomeSummary
def _ev(has_turns=True, all_replied=True, result_passes=()):
return CaseEvidence(has_turns=has_turns, all_replied=all_replied, result_passes=tuple(result_passes))
# ── authoritative wins ──────────────────────────────────────────────────────
def test_authoritative_outcome_used_verbatim_ignoring_evidence():
# Evidence would approximate passed=True (all rules pass), but the authoritative
# verdict says failed (e.g. WEIGHTED/ANY logic) — authority must win.
verdicts = resolve_case_verdicts(
case_outcomes={"c1": CaseOutcomeSummary(passed=False, connectivity=False)},
evidence={"c1": _ev(result_passes=(True, True))},
errored_case_ids=set(),
)
assert verdicts["c1"] == CaseOutcomeSummary(passed=False, connectivity=False)
def test_authoritative_connectivity_preserved():
verdicts = resolve_case_verdicts(
case_outcomes={"c1": CaseOutcomeSummary(passed=True, connectivity=True)},
evidence={"c1": _ev(result_passes=(False,))},
errored_case_ids=set(),
)
assert verdicts["c1"].connectivity is True
assert verdicts["c1"].passed is True
# ── legacy approximation (case_outcomes missing) ────────────────────────────
def test_legacy_connectivity_case_passes():
# No judged results, has turns, every turn replied, not errored → connectivity pass.
verdicts = resolve_case_verdicts(
case_outcomes={},
evidence={"c1": _ev(has_turns=True, all_replied=True, result_passes=())},
errored_case_ids=set(),
)
assert verdicts["c1"] == CaseOutcomeSummary(passed=True, connectivity=True)
def test_legacy_all_rules_pass():
verdicts = resolve_case_verdicts(
case_outcomes={},
evidence={"c1": _ev(result_passes=(True, True))},
errored_case_ids=set(),
)
assert verdicts["c1"] == CaseOutcomeSummary(passed=True, connectivity=False)
def test_legacy_some_rule_fails():
verdicts = resolve_case_verdicts(
case_outcomes={},
evidence={"c1": _ev(result_passes=(True, False))},
errored_case_ids=set(),
)
assert verdicts["c1"] == CaseOutcomeSummary(passed=False, connectivity=False)
def test_legacy_fault_no_results_not_all_replied():
# No results and a turn missing its reply → fault, not connectivity → fail (ADR-0002).
verdicts = resolve_case_verdicts(
case_outcomes={},
evidence={"c1": _ev(has_turns=True, all_replied=False, result_passes=())},
errored_case_ids=set(),
)
assert verdicts["c1"] == CaseOutcomeSummary(passed=False, connectivity=False)
def test_legacy_errored_case_not_connectivity():
# A case with a case-level error is not connectivity even if it replied.
verdicts = resolve_case_verdicts(
case_outcomes={},
evidence={"c1": _ev(has_turns=True, all_replied=True, result_passes=())},
errored_case_ids={"c1"},
)
assert verdicts["c1"] == CaseOutcomeSummary(passed=False, connectivity=False)
def test_legacy_no_turns_no_results_is_fault():
verdicts = resolve_case_verdicts(
case_outcomes={},
evidence={"c1": _ev(has_turns=False, all_replied=True, result_passes=())},
errored_case_ids=set(),
)
assert verdicts["c1"] == CaseOutcomeSummary(passed=False, connectivity=False)
# ── mixed authoritative + legacy in one call ────────────────────────────────
def test_mixed_authoritative_and_legacy():
verdicts = resolve_case_verdicts(
case_outcomes={"auth": CaseOutcomeSummary(passed=True, connectivity=False)},
evidence={
"auth": _ev(result_passes=(False,)), # authority overrides
"legacy": _ev(result_passes=(True,)), # approximated
},
errored_case_ids=set(),
)
assert verdicts["auth"] == CaseOutcomeSummary(passed=True, connectivity=False)
assert verdicts["legacy"] == CaseOutcomeSummary(passed=True, connectivity=False)
# ── build_case_evidence ──────────────────────────────────────────────────────
class _FakeTurn:
def __init__(self, case_id: str, has_reply: bool = True):
self.case_id = case_id
self._reply = {"msgBody": {"content": "ok"}} if has_reply else None
def get_reply(self):
return self._reply
class _FakeResult:
def __init__(self, case_id: str, passed: bool):
self.case_id = case_id
self.passed = passed
def test_build_evidence_turns_only_no_results():
"""Turns present, no results → has_turns=True, result_passes=()."""
turns = [_FakeTurn("c-1"), _FakeTurn("c-1")]
results: list = []
evidence = build_case_evidence(turns, results)
assert evidence == {
"c-1": CaseEvidence(has_turns=True, all_replied=True, result_passes=()),
}
def test_build_evidence_missing_reply_sets_all_replied_false():
"""A turn with no reply → all_replied=False for that case."""
turns = [_FakeTurn("c-1", has_reply=True), _FakeTurn("c-1", has_reply=False)]
results: list = []
evidence = build_case_evidence(turns, results)
assert evidence["c-1"].all_replied is False
assert evidence["c-1"].has_turns is True
def test_build_evidence_turns_and_results_aggregate_passes():
"""Turns + results → result_passes aggregates all rule passes for the case."""
turns = [_FakeTurn("c-1")]
results = [_FakeResult("c-1", passed=True), _FakeResult("c-1", passed=False)]
evidence = build_case_evidence(turns, results)
assert evidence["c-1"].result_passes == (True, False)
assert evidence["c-1"].has_turns is True
assert evidence["c-1"].all_replied is True
def test_build_evidence_results_without_turns():
"""Results exist but no turns → has_turns=False."""
turns: list = []
results = [_FakeResult("c-1", passed=True)]
evidence = build_case_evidence(turns, results)
assert evidence["c-1"].has_turns is False
assert evidence["c-1"].all_replied is True
assert evidence["c-1"].result_passes == (True,)
def test_build_evidence_multiple_cases():
"""Multiple cases are grouped independently."""
turns = [_FakeTurn("c-1"), _FakeTurn("c-2", has_reply=False)]
results = [_FakeResult("c-1", passed=True), _FakeResult("c-2", passed=False)]
evidence = build_case_evidence(turns, results)
assert len(evidence) == 2
assert evidence["c-1"] == CaseEvidence(has_turns=True, all_replied=True, result_passes=(True,))
assert evidence["c-2"] == CaseEvidence(has_turns=True, all_replied=False, result_passes=(False,))