- EvalRun.triggered_by 全链路(manual/ai_assistant/cli)+ 迁移 b7d4e6f81c22 - 标准 agenteval-run SKILL.md 纳入版本管理,deploy 脚本同步 + API Key 注入 - 简单登录:AGENTEVAL_ADMIN_PASSWORD + HMAC 会话 token,require_auth 双凭据 - 对比报告限同场景(400)+ 空 results 误判修复 - /api/stats/dashboard 扩展聚合;/api/runs 返回场景/对象名 - 测试 218 → 232
251 lines
8.8 KiB
Python
251 lines
8.8 KiB
Python
"""Unit tests for report generation: generate_report, generate_compare_report, render_markdown_report."""
|
|
|
|
import pytest
|
|
from sqlmodel import Session, SQLModel, create_engine
|
|
|
|
from agenteval.evaluation.report import (
|
|
generate_compare_report,
|
|
generate_report,
|
|
render_markdown_report,
|
|
render_json_report,
|
|
)
|
|
from agenteval.models import (
|
|
Case, CaseType, EvalResult, EvalRun, EvalTarget, RunStatus, Scenario, Turn,
|
|
PlatformType, ChannelType, TargetStatus,
|
|
)
|
|
from agenteval.storage.repository import ResultRepository, RunRepository, ScenarioRepository, TargetRepository
|
|
|
|
|
|
@pytest.fixture()
|
|
def report_session(tmp_path):
|
|
from agenteval.storage.db import ( # noqa: F401
|
|
EvalResultDB, EvalRunDB, EvalTargetDB, FileCategoryDB, FileRecordDB, ScenarioDB, TurnDB,
|
|
)
|
|
engine = create_engine(
|
|
f"sqlite:///{tmp_path / 'report_test.db'}",
|
|
connect_args={"check_same_thread": False},
|
|
)
|
|
SQLModel.metadata.create_all(engine)
|
|
session = Session(engine)
|
|
try:
|
|
yield session
|
|
finally:
|
|
session.close()
|
|
engine.dispose()
|
|
|
|
|
|
def _seed_run(
|
|
session: Session,
|
|
*,
|
|
pass_rate: float = 1.0,
|
|
n_cases: int = 1,
|
|
scenario_id: str | None = None,
|
|
) -> str:
|
|
"""Create a minimal completed run with real data in the DB and return run_id."""
|
|
target = EvalTarget(
|
|
name="测试对象",
|
|
platform=PlatformType.AI_DIGITAL_EMPLOYEE,
|
|
channel_type=ChannelType.TUTU_API,
|
|
channel_config={},
|
|
status=TargetStatus.ACTIVE,
|
|
)
|
|
target = TargetRepository(session).create(target)
|
|
|
|
if scenario_id is None:
|
|
scenario = Scenario(
|
|
name="测试场景",
|
|
cases=[Case(id=f"c{i}", type=CaseType.SINGLE, messages=["hi"]) for i in range(n_cases)],
|
|
)
|
|
scenario = ScenarioRepository(session).create(scenario)
|
|
scenario_id = scenario.id
|
|
|
|
run = EvalRun(
|
|
target_id=target.id,
|
|
scenario_id=scenario_id,
|
|
status=RunStatus.COMPLETED,
|
|
)
|
|
run = RunRepository(session).create(run)
|
|
|
|
run_repo = RunRepository(session)
|
|
result_repo = ResultRepository(session)
|
|
|
|
total = n_cases
|
|
passed = int(total * pass_rate)
|
|
|
|
for i in range(n_cases):
|
|
turn = Turn(
|
|
run_id=run.id,
|
|
case_id=f"c{i}",
|
|
round_index=1,
|
|
sent_message={"msgBody": {"content": f"问题{i}"}},
|
|
reply={"msgBody": {"content": f"回答{i}"}},
|
|
latency_ms=200,
|
|
)
|
|
result_repo.save_turn(turn)
|
|
db_turn = run_repo.get_turns(run.id)[-1]
|
|
|
|
eval_result = EvalResult(
|
|
run_id=run.id,
|
|
case_id=f"c{i}",
|
|
turn_id=db_turn.id or "",
|
|
rule_type="keyword_match",
|
|
passed=(i < passed),
|
|
score=1.0 if i < passed else 0.0,
|
|
reason="通过" if i < passed else "失败",
|
|
)
|
|
result_repo.save_result(eval_result)
|
|
|
|
run.summary = {
|
|
"total_cases": total,
|
|
"passed_cases": passed,
|
|
"failed_cases": total - passed,
|
|
"total_rules": total,
|
|
"passed_rules": passed,
|
|
"pass_rate": round(pass_rate, 4),
|
|
}
|
|
RunRepository(session).update(run)
|
|
return run.id
|
|
|
|
|
|
# ── generate_report ───────────────────────────────────────────────────────
|
|
|
|
def test_generate_report_structure(report_session):
|
|
run_id = _seed_run(report_session)
|
|
report = generate_report(run_id, report_session)
|
|
|
|
assert report["run_id"] == run_id
|
|
assert report["target_name"] == "测试对象"
|
|
assert report["scenario_name"] == "测试场景"
|
|
assert report["status"] == "completed"
|
|
assert "summary" in report
|
|
assert "cases" in report
|
|
|
|
|
|
def test_generate_report_summary_values(report_session):
|
|
run_id = _seed_run(report_session, pass_rate=1.0, n_cases=2)
|
|
report = generate_report(run_id, report_session)
|
|
s = report["summary"]
|
|
assert s["total_cases"] == 2
|
|
assert s["passed_cases"] == 2
|
|
assert s["pass_rate"] == 1.0
|
|
|
|
|
|
def test_generate_report_partial_pass(report_session):
|
|
run_id = _seed_run(report_session, pass_rate=0.5, n_cases=2)
|
|
report = generate_report(run_id, report_session)
|
|
s = report["summary"]
|
|
assert s["passed_cases"] == 1
|
|
assert s["failed_cases"] == 1
|
|
|
|
|
|
def test_generate_report_cases_contain_turns_and_results(report_session):
|
|
run_id = _seed_run(report_session, n_cases=1)
|
|
report = generate_report(run_id, report_session)
|
|
assert len(report["cases"]) == 1
|
|
case = report["cases"][0]
|
|
assert len(case["turns"]) == 1
|
|
assert len(case["results"]) == 1
|
|
assert case["turns"][0]["sent_text"] == "问题0"
|
|
assert case["turns"][0]["latency_ms"] == 200
|
|
|
|
|
|
def test_generate_report_not_found_raises(report_session):
|
|
with pytest.raises(ValueError, match="run not found"):
|
|
generate_report("no-such-id", report_session)
|
|
|
|
|
|
# ── generate_compare_report ───────────────────────────────────────────────
|
|
|
|
def _scenario_of(session: Session, run_id: str) -> str:
|
|
return RunRepository(session).get(run_id).scenario_id
|
|
|
|
|
|
def test_compare_report_structure(report_session):
|
|
run_id_a = _seed_run(report_session, pass_rate=1.0, n_cases=2)
|
|
sid = _scenario_of(report_session, run_id_a)
|
|
run_id_b = _seed_run(report_session, pass_rate=0.5, n_cases=2, scenario_id=sid)
|
|
result = generate_compare_report(run_id_a, run_id_b, report_session)
|
|
|
|
assert "run_a" in result
|
|
assert "run_b" in result
|
|
assert "delta" in result
|
|
assert "cases" in result
|
|
assert result["run_a"]["run_id"] == run_id_a
|
|
assert result["run_b"]["run_id"] == run_id_b
|
|
|
|
|
|
def test_compare_report_delta(report_session):
|
|
run_id_a = _seed_run(report_session, pass_rate=0.5, n_cases=2)
|
|
sid = _scenario_of(report_session, run_id_a)
|
|
run_id_b = _seed_run(report_session, pass_rate=1.0, n_cases=2, scenario_id=sid)
|
|
result = generate_compare_report(run_id_a, run_id_b, report_session)
|
|
assert result["delta"]["pass_rate"] > 0 # B improved over A
|
|
|
|
|
|
def test_compare_report_changed_cases(report_session):
|
|
run_id_a = _seed_run(report_session, pass_rate=1.0, n_cases=2)
|
|
sid = _scenario_of(report_session, run_id_a)
|
|
run_id_b = _seed_run(report_session, pass_rate=0.5, n_cases=2, scenario_id=sid)
|
|
result = generate_compare_report(run_id_a, run_id_b, report_session)
|
|
# At least one case changed (A all-pass vs B half-pass)
|
|
assert result["changed_cases"] >= 1
|
|
|
|
|
|
def test_compare_report_case_level(report_session):
|
|
run_id_a = _seed_run(report_session, n_cases=1)
|
|
sid = _scenario_of(report_session, run_id_a)
|
|
run_id_b = _seed_run(report_session, n_cases=1, scenario_id=sid)
|
|
result = generate_compare_report(run_id_a, run_id_b, report_session)
|
|
assert len(result["cases"]) >= 1
|
|
case = result["cases"][0]
|
|
assert "run_a_passed" in case
|
|
assert "run_b_passed" in case
|
|
assert "changed" in case
|
|
|
|
|
|
def test_compare_report_different_scenarios_rejected(report_session):
|
|
run_id_a = _seed_run(report_session, n_cases=1)
|
|
run_id_b = _seed_run(report_session, n_cases=1) # separate scenario
|
|
with pytest.raises(ValueError, match="same scenario"):
|
|
generate_compare_report(run_id_a, run_id_b, report_session)
|
|
|
|
|
|
# ── render_markdown_report ────────────────────────────────────────────────
|
|
|
|
def test_render_markdown_contains_header(report_session):
|
|
run_id = _seed_run(report_session)
|
|
md = render_markdown_report(run_id, report_session)
|
|
assert "# 评测报告" in md
|
|
|
|
|
|
def test_render_markdown_contains_summary_table(report_session):
|
|
run_id = _seed_run(report_session)
|
|
md = render_markdown_report(run_id, report_session)
|
|
assert "## 汇总" in md
|
|
assert "| 指标 | 数值 |" in md
|
|
assert "通过率" in md
|
|
|
|
|
|
def test_render_markdown_contains_case_section(report_session):
|
|
run_id = _seed_run(report_session, n_cases=1)
|
|
md = render_markdown_report(run_id, report_session)
|
|
assert "## 用例明细" in md
|
|
assert "### " in md # case header
|
|
|
|
|
|
def test_render_markdown_contains_rule_table(report_session):
|
|
run_id = _seed_run(report_session)
|
|
md = render_markdown_report(run_id, report_session)
|
|
assert "**规则评估结果**" in md
|
|
assert "keyword_match" in md
|
|
|
|
|
|
# ── render_json_report ────────────────────────────────────────────────────
|
|
|
|
def test_render_json_report_is_valid_json(report_session):
|
|
import json
|
|
run_id = _seed_run(report_session)
|
|
json_text = render_json_report(run_id, report_session)
|
|
parsed = json.loads(json_text)
|
|
assert parsed["run_id"] == run_id
|