AgentEvalTool/tests/unit/test_metrics.py
sinohqb 782916a283 refactor(metrics): type Run summary and converge cross-run aggregation
Give EvalRun.summary a typed RunSummary value (unified RunError, lenient
legacy parsing) so readers stop reaching into a schemaless dict, and route
every cross-run rollup — dashboard, scenario ranking, trend, campaign
report — through one aggregate_runs seam. Fixes the divergence where
stats averaged pass_rate over completed-only runs while the campaign
report counted faults as 0.0. Cross-run rule (ADR-0004): genuine faults
count 0.0, user-cancelled runs are excluded from both denominators.
2026-07-31 01:57:56 +08:00

130 lines
4.7 KiB
Python

"""Unit tests for the RunSummary value type and the single cross-run
aggregation seam (ADR-0004: faults count 0.0, user-cancelled excluded)."""
from agenteval.evaluation.metrics import aggregate_runs
from agenteval.models import EvalRun, RunStatus, RunSummary
# ── RunSummary parsing (legacy dict rows must keep parsing) ────────────────
def test_parses_full_engine_summary():
s = RunSummary.model_validate({
"total_cases": 3,
"passed_cases": 2,
"failed_cases": 1,
"total_rules": 5,
"passed_rules": 4,
"pass_rate": 0.6667,
"avg_latency_ms": 123.4,
"case_outcomes": {"c1": {"passed": True, "connectivity": False}},
"case_errors": [{"case_id": "c2", "stage": "generate_messages", "error": "boom"}],
"model_configs": {"judge": {"model": "m"}},
})
assert s.pass_rate == 0.6667
assert s.avg_latency_ms == 123.4
assert s.case_outcomes["c1"].passed is True
assert s.case_outcomes["c1"].connectivity is False
assert s.error is None
def test_parses_legacy_string_error():
s = RunSummary.model_validate({"error": "channel exploded"})
assert s.error is not None
assert s.error.code == "error"
assert s.error.message == "channel exploded"
assert s.is_cancelled is False
def test_parses_object_error_and_detects_cancel():
s = RunSummary.model_validate({"error": {"code": "cancelled_by_user", "message": "评测已手动停止"}})
assert s.error is not None
assert s.error.code == "cancelled_by_user"
assert s.is_cancelled is True
def test_missing_keys_default_and_unknown_keys_tolerated():
s = RunSummary.model_validate({"pass_rate": 0.5, "some_future_key": 1})
assert s.pass_rate == 0.5
assert s.total_cases == 0
assert s.case_outcomes == {}
assert s.error is None
def test_eval_run_summary_field_is_typed():
run = EvalRun(target_id="t", scenario_id="s", summary={"pass_rate": 1.0})
assert isinstance(run.summary, RunSummary)
assert run.summary.pass_rate == 1.0
# ── aggregate_runs (ADR-0004) ──────────────────────────────────────────────
def _run(status, *, pass_rate=None, latency=None, error=None) -> EvalRun:
summary = None
if pass_rate is not None or latency is not None or error is not None:
summary = {}
if pass_rate is not None:
summary["pass_rate"] = pass_rate
if latency is not None:
summary["avg_latency_ms"] = latency
if error is not None:
summary["error"] = error
return EvalRun(target_id="t", scenario_id="s", status=status, summary=summary)
def test_empty_runs_aggregate_to_none():
agg = aggregate_runs([])
assert agg == {"run_count": 0, "pass_rate": None, "availability": None, "avg_latency_ms": None}
def test_completed_runs_average_pass_rate_and_latency():
agg = aggregate_runs([
_run(RunStatus.COMPLETED, pass_rate=1.0, latency=100),
_run(RunStatus.COMPLETED, pass_rate=0.5, latency=200),
])
assert agg["run_count"] == 2
assert agg["pass_rate"] == 0.75
assert agg["availability"] == 1.0
assert agg["avg_latency_ms"] == 150.0
def test_faulted_run_counts_zero_in_both_denominators():
# ADR-0004: a genuine execution fault drags pass_rate AND availability down.
agg = aggregate_runs([
_run(RunStatus.COMPLETED, pass_rate=1.0),
_run(RunStatus.FAILED, error="channel exploded"),
])
assert agg["pass_rate"] == 0.5
assert agg["availability"] == 0.5
def test_cancelled_run_excluded_from_denominators():
# ADR-0004: user cancellation is neither a quality nor availability signal.
agg = aggregate_runs([
_run(RunStatus.COMPLETED, pass_rate=1.0),
_run(RunStatus.FAILED, error={"code": "cancelled_by_user", "message": "stop"}),
])
assert agg["run_count"] == 2 # what happened stays visible
assert agg["pass_rate"] == 1.0 # cancelled run out of the denominator
assert agg["availability"] == 1.0
def test_all_cancelled_aggregates_to_none():
agg = aggregate_runs([
_run(RunStatus.FAILED, error={"code": "cancelled_by_user", "message": "stop"}),
])
assert agg["run_count"] == 1
assert agg["pass_rate"] is None
assert agg["availability"] is None
assert agg["avg_latency_ms"] is None
def test_in_flight_runs_count_zero_not_cancelled():
# pending/running (no summary) are not cancelled — they count 0.0 for now.
agg = aggregate_runs([
_run(RunStatus.RUNNING),
_run(RunStatus.PENDING),
])
assert agg["pass_rate"] == 0.0
assert agg["availability"] == 0.0