AgentEvalTool/tests/integration/test_reports_api.py
sinohqb e0b69fa2b9 v0.4-t1t2: 测试覆盖率 62%→77% + UTC 时区根本修复
## T1: P0 测试补全(+67 个测试)
- test_utils_llm.py: extract_reply_text / extract_content_from_llm_response / parse_json_from_llm_text 各边界
- test_file_repository.py: 分类 CRUD / 树形结构 / 级联删除 / 文件创建/查询/删除/物理文件清理
- test_report.py: generate_report / generate_compare_report / render_markdown / render_json
- test_llm_score.py: OpenAI 格式 / Anthropic content-block 格式 / JSON 回退解析 / 异常降级

## T2: P1 测试补全(+28 个测试)
- test_scenarios.py: 模板列表/字段完整性/规则类型有效性 + YAML/JSON 加载/校验
- test_webhook.py: 未配置不发送 / 正确 payload / secret header / 异常静默忽略
- test_reports_api.py: GET /reports/{id} / /html / /json / /markdown / /compare 集成测试

## UTC 时区根本修复
- storage/db.py: 新增 iso_utc() 函数,确保所有 datetime 序列化输出带 Z 后缀
- runs.py / files.py / report.py: 6 处 .isoformat() → iso_utc()
- 前端 toDate() 兜底仍保留(向下兼容),但后端不再输出无时区时间戳

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-17 14:19:16 +08:00

161 lines
4.9 KiB
Python

"""Integration tests for the reports API: /api/reports/{id}, /markdown, /compare."""
import json
import pytest
from fastapi.testclient import TestClient
from sqlmodel import Session, SQLModel, create_engine
from agenteval.models import (
Case, CaseType, EvalResult, EvalRun, EvalTarget, RunStatus, Scenario,
PlatformType, ChannelType, TargetStatus, Turn,
)
from agenteval.storage.repository import ResultRepository, RunRepository, ScenarioRepository, TargetRepository
from agenteval.web.app import app
from agenteval.web.deps import get_db
@pytest.fixture()
def client_with_db(tmp_path):
from agenteval.storage.db import ( # noqa: F401
EvalResultDB, EvalRunDB, EvalTargetDB, FileCategoryDB, FileRecordDB, ScenarioDB, TurnDB,
)
engine = create_engine(
f"sqlite:///{tmp_path / 'reports_api.db'}",
connect_args={"check_same_thread": False},
)
SQLModel.metadata.create_all(engine)
session = Session(engine)
def override_get_db():
try:
yield session
finally:
pass
app.dependency_overrides[get_db] = override_get_db
client = TestClient(app)
yield client, session
app.dependency_overrides.clear()
session.close()
def _seed_run(session: Session, name_suffix: str = "") -> str:
target = EvalTarget(
name=f"target{name_suffix}",
platform=PlatformType.AI_DIGITAL_EMPLOYEE,
channel_type=ChannelType.TUTU_API,
channel_config={},
status=TargetStatus.ACTIVE,
)
target = TargetRepository(session).create(target)
scenario = Scenario(
name=f"scenario{name_suffix}",
cases=[Case(id="c1", type=CaseType.SINGLE, messages=["hi"])],
)
scenario = ScenarioRepository(session).create(scenario)
run = EvalRun(
target_id=target.id,
scenario_id=scenario.id,
status=RunStatus.COMPLETED,
)
run = RunRepository(session).create(run)
result_repo = ResultRepository(session)
run_repo = RunRepository(session)
turn = Turn(
run_id=run.id, case_id="c1", round_index=1,
sent_message={"msgBody": {"content": "问题"}},
reply={"msgBody": {"content": "回答"}},
latency_ms=300,
)
result_repo.save_turn(turn)
db_turn = run_repo.get_turns(run.id)[0]
result_repo.save_result(EvalResult(
run_id=run.id, case_id="c1",
turn_id=db_turn.id or "",
rule_type="response_time",
passed=True, score=0.9, reason="",
))
run.summary = {
"total_cases": 1, "passed_cases": 1, "failed_cases": 0,
"total_rules": 1, "passed_rules": 1, "pass_rate": 1.0,
}
RunRepository(session).update(run)
return run.id
def test_get_report_200(client_with_db):
client, session = client_with_db
run_id = _seed_run(session)
resp = client.get(f"/api/reports/{run_id}")
assert resp.status_code == 200
data = resp.json()
assert data["run_id"] == run_id
assert data["summary"]["pass_rate"] == 1.0
def test_get_report_404(client_with_db):
client, _ = client_with_db
resp = client.get("/api/reports/no-such-run")
assert resp.status_code == 404
def test_get_html_report(client_with_db):
client, session = client_with_db
run_id = _seed_run(session)
resp = client.get(f"/api/reports/{run_id}/html")
assert resp.status_code == 200
assert "text/html" in resp.headers["content-type"]
assert "评测报告" in resp.text
def test_get_json_report(client_with_db):
client, session = client_with_db
run_id = _seed_run(session)
resp = client.get(f"/api/reports/{run_id}/json")
assert resp.status_code == 200
data = json.loads(resp.text)
assert "run_id" in data
def test_get_markdown_report(client_with_db):
client, session = client_with_db
run_id = _seed_run(session)
resp = client.get(f"/api/reports/{run_id}/markdown")
assert resp.status_code == 200
assert "text/markdown" in resp.headers["content-type"]
assert "# 评测报告" in resp.text
assert "## 汇总" in resp.text
def test_get_markdown_report_attachment_header(client_with_db):
client, session = client_with_db
run_id = _seed_run(session)
resp = client.get(f"/api/reports/{run_id}/markdown")
assert "attachment" in resp.headers.get("content-disposition", "")
def test_compare_report(client_with_db):
client, session = client_with_db
run_id_a = _seed_run(session, "A")
run_id_b = _seed_run(session, "B")
resp = client.get(f"/api/reports/compare?run1={run_id_a}&run2={run_id_b}")
assert resp.status_code == 200
data = resp.json()
assert data["run_a"]["run_id"] == run_id_a
assert data["run_b"]["run_id"] == run_id_b
assert "delta" in data
assert "cases" in data
def test_compare_report_run_not_found(client_with_db):
client, session = client_with_db
run_id = _seed_run(session)
resp = client.get(f"/api/reports/compare?run1={run_id}&run2=ghost-id")
assert resp.status_code == 404