## 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>
161 lines
4.9 KiB
Python
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
|