report.py mixed DB-reading generation with string formatting: the four render_*_report(run_id, session) functions each re-fetched via generate_report, so the HTML/Markdown/JSON formatting was welded to storage and could not be unit-tested from a plain dict. Extract the formatting into a new pure report_render module whose renderers take the already-built report dict (no session, no storage import). Migrate every caller to generate-then- render, delete the old coupled renderers with no back-compat shim, and drop the _aggregate_runs middle-man alias in favour of metrics.aggregate_runs.
188 lines
6.2 KiB
Python
188 lines
6.2 KiB
Python
"""Unit tests for the pure renderers: dict in, HTML/Markdown/JSON out (no DB)."""
|
||
|
||
import json
|
||
|
||
from agenteval.evaluation.report_render import (
|
||
render_campaign_markdown,
|
||
render_html,
|
||
render_json,
|
||
render_markdown,
|
||
)
|
||
|
||
|
||
def _run_report(**overrides) -> dict:
|
||
"""A hand-built report dict matching generate_report's shape."""
|
||
report = {
|
||
"run_id": "run-1",
|
||
"target_id": "t-1",
|
||
"target_name": "客服机器人",
|
||
"scenario_id": "s-1",
|
||
"scenario_name": "售后场景",
|
||
"scenario_version": 2,
|
||
"triggered_by": "manual",
|
||
"status": "completed",
|
||
"started_at": "2026-07-30T10:00:00+00:00",
|
||
"completed_at": "2026-07-30T10:05:00+00:00",
|
||
"summary": {
|
||
"total_cases": 2,
|
||
"passed_cases": 1,
|
||
"failed_cases": 1,
|
||
"total_rules": 3,
|
||
"passed_rules": 2,
|
||
"pass_rate": 0.5,
|
||
"connectivity_cases": 1,
|
||
"judged_pass_rate": 0.0,
|
||
},
|
||
"cases": [
|
||
{
|
||
"case_id": "case-a",
|
||
"passed": True,
|
||
"connectivity": True,
|
||
"turns": [
|
||
{
|
||
"round": 0,
|
||
"sent_text": "你好",
|
||
"reply_text": "您好,请问有什么可以帮您?",
|
||
"latency_ms": 120,
|
||
"question_msg_id": "m-1",
|
||
}
|
||
],
|
||
"results": [],
|
||
},
|
||
{
|
||
"case_id": "case-b",
|
||
"passed": False,
|
||
"connectivity": False,
|
||
"turns": [
|
||
{
|
||
"round": 0,
|
||
"sent_text": "退货流程",
|
||
"reply_text": None,
|
||
"latency_ms": None,
|
||
"question_msg_id": "m-2",
|
||
}
|
||
],
|
||
"results": [
|
||
{"rule_type": "keyword_match", "passed": False, "score": 0.0, "reason": "缺少关键词"},
|
||
],
|
||
},
|
||
],
|
||
}
|
||
report.update(overrides)
|
||
return report
|
||
|
||
|
||
def _campaign_report() -> dict:
|
||
"""A hand-built dict matching generate_campaign_report's shape."""
|
||
return {
|
||
"campaign_id": "c-1",
|
||
"name": "夜间巡检",
|
||
"target_id": "t-1",
|
||
"status": "completed",
|
||
"window_seconds": 7200,
|
||
"time_scale": 1.0,
|
||
"started_at": "2026-07-30T00:00:00+00:00",
|
||
"completed_at": "2026-07-30T02:00:00+00:00",
|
||
"summary": {
|
||
"total_runs": 2,
|
||
"completed_runs": 2,
|
||
"overall_pass_rate": 0.75,
|
||
"overall_availability": 1.0,
|
||
"avg_latency_ms": 150.0,
|
||
},
|
||
"time_trend": [
|
||
{
|
||
"bucket_index": 0,
|
||
"start_seconds": 0.0,
|
||
"end_seconds": 3600.0,
|
||
"run_count": 2,
|
||
"pass_rate": 0.75,
|
||
"availability": 1.0,
|
||
"avg_latency_ms": 150.0,
|
||
},
|
||
{
|
||
"bucket_index": 1,
|
||
"start_seconds": 3600.0,
|
||
"end_seconds": 7200.0,
|
||
"run_count": 0,
|
||
"pass_rate": None,
|
||
"availability": None,
|
||
"avg_latency_ms": None,
|
||
},
|
||
],
|
||
"capability_summary": [
|
||
{
|
||
"scenario_id": "s-1",
|
||
"scenario_name": "售后场景",
|
||
"run_count": 2,
|
||
"pass_rate": 0.75,
|
||
"availability": 1.0,
|
||
"avg_latency_ms": 150.0,
|
||
},
|
||
],
|
||
}
|
||
|
||
|
||
# ── render_html ─────────────────────────────────────────────────────────────
|
||
|
||
def test_render_html_contains_names_and_summary():
|
||
html = render_html(_run_report())
|
||
assert "客服机器人" in html
|
||
assert "售后场景" in html
|
||
assert "50.00%" in html # pass_rate 0.5
|
||
|
||
|
||
def test_render_html_contains_turns_and_rule_badges():
|
||
html = render_html(_run_report())
|
||
assert "退货流程" in html
|
||
assert "keyword_match" in html
|
||
assert "失败" in html
|
||
|
||
|
||
# ── render_markdown ─────────────────────────────────────────────────────────
|
||
|
||
def test_render_markdown_summary_table():
|
||
md = render_markdown(_run_report())
|
||
assert "| 总用例数 | 2 |" in md
|
||
assert "| 通过率 | 50.0% |" in md
|
||
assert "| 连通用例 | 1 |" in md
|
||
|
||
|
||
def test_render_markdown_connectivity_badge_and_no_reply():
|
||
md = render_markdown(_run_report())
|
||
assert "🔗" in md # connectivity case badge
|
||
assert "(连通用例,未配置判定标准)" in md
|
||
assert "(无回复)" in md
|
||
|
||
|
||
def test_render_markdown_judged_pass_rate_dash_when_none():
|
||
report = _run_report()
|
||
report["summary"]["judged_pass_rate"] = None
|
||
md = render_markdown(report)
|
||
assert "| 判定型通过率 | — |" in md
|
||
|
||
|
||
# ── render_json ─────────────────────────────────────────────────────────────
|
||
|
||
def test_render_json_roundtrips():
|
||
report = _run_report()
|
||
parsed = json.loads(render_json(report))
|
||
assert parsed == report
|
||
|
||
|
||
# ── render_campaign_markdown ────────────────────────────────────────────────
|
||
|
||
def test_render_campaign_markdown_summary_and_axes():
|
||
md = render_campaign_markdown(_campaign_report())
|
||
assert "# 活动周期报告 — 夜间巡检" in md
|
||
assert "| 整窗通过率 | 75.0% |" in md
|
||
assert "## 时间趋势" in md
|
||
assert "## 能力汇总" in md
|
||
assert "| 售后场景 | 2 | 75.0% | 100.0% | 150ms |" in md
|
||
|
||
|
||
def test_render_campaign_markdown_empty_bucket_dashes():
|
||
md = render_campaign_markdown(_campaign_report())
|
||
# bucket 1 has no runs: pass_rate/availability/latency all render as —
|
||
assert "| 3600–7200 | 0 | — | — | — |" in md
|