feat(report): compare requires same scenario version (ticket 05)
Some checks failed
CI / test (push) Failing after 39s
Some checks failed
CI / test (push) Failing after 39s
对比报告可比性收紧为同场景同考纲版本(ADR-0001):跨版本 API 返回 400 (detail 含双方版本号),报告生成层抛 ValueError;前端对比候选按 同场景 + 同版本过滤,A 变更后自动清空不可比的 B。文档"尚未实现"标注移除。
This commit is contained in:
parent
0a47260237
commit
770d260750
@ -4,10 +4,10 @@
|
||||
|
||||
**Blocked by:** 04 — 运行记录场景版本。
|
||||
|
||||
**Status:** ready-for-agent
|
||||
**Status:** done
|
||||
|
||||
- [ ] 同场景不同版本的两次运行对比:API 400,detail 含双方版本号
|
||||
- [ ] 同场景同版本对比正常生成(含动态用例场景——同考纲即可比)
|
||||
- [ ] 报告生成层对跨版本对比抛出明确错误
|
||||
- [ ] 前端对比候选按同场景 + 同版本过滤;跨版本被拒时提示可读
|
||||
- [ ] 集成与单元测试覆盖拒绝与放行两侧(先例:现有对比报告测试)
|
||||
- [x] 同场景不同版本的两次运行对比:API 400,detail 含双方版本号
|
||||
- [x] 同场景同版本对比正常生成(含动态用例场景——同考纲即可比)
|
||||
- [x] 报告生成层对跨版本对比抛出明确错误
|
||||
- [x] 前端对比候选按同场景 + 同版本过滤;跨版本被拒时提示可读
|
||||
- [x] 集成与单元测试覆盖拒绝与放行两侧(先例:现有对比报告测试)
|
||||
|
||||
@ -41,7 +41,7 @@ _Avoid_: 测试点、题目
|
||||
_Avoid_: 差异报告
|
||||
|
||||
**场景版本(Scenario Version)**:
|
||||
场景考纲的版本标识。仅考纲字段(用例集、模型绑定、LLM 配置)变更时递增;名称、描述、标签等元数据编辑不升版。(决策见 ADR-0001,尚未实现)
|
||||
场景考纲的版本标识。仅考纲字段(用例集、模型绑定、LLM 配置)变更时递增;名称、描述、标签等元数据编辑不升版。(决策见 ADR-0001)
|
||||
_Avoid_: 修订号
|
||||
|
||||
**评测运行(Run / EvalRun)**:
|
||||
|
||||
@ -199,6 +199,12 @@ def generate_compare_report(run_id_1: str, run_id_2: str, session=None) -> dict[
|
||||
# "changed" and the diff would be meaningless — reject early.
|
||||
if report_a.get("scenario_id") != report_b.get("scenario_id"):
|
||||
raise ValueError("compare report requires both runs to use the same scenario")
|
||||
# 同场景还须同考纲版本才可比(ADR-0001)
|
||||
if report_a.get("scenario_version") != report_b.get("scenario_version"):
|
||||
raise ValueError(
|
||||
"compare report requires the same scenario version "
|
||||
f"(A: v{report_a.get('scenario_version')}, B: v{report_b.get('scenario_version')})"
|
||||
)
|
||||
|
||||
def _summary_delta(key: str) -> float:
|
||||
return report_b["summary"][key] - report_a["summary"][key]
|
||||
|
||||
@ -31,6 +31,12 @@ def get_compare_report(
|
||||
raise HTTPException(status_code=404, detail=f"run not found: {run2}")
|
||||
if run_a.scenario_id != run_b.scenario_id:
|
||||
raise HTTPException(status_code=400, detail="对比报告要求两个运行使用相同场景")
|
||||
if run_a.scenario_version != run_b.scenario_version:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"对比报告要求相同的场景考纲版本(A: v{run_a.scenario_version}, B: v{run_b.scenario_version}),"
|
||||
"考纲变更前后的运行不可比",
|
||||
)
|
||||
return generate_compare_report(run1, run2, session)
|
||||
|
||||
|
||||
|
||||
@ -10,5 +10,5 @@
|
||||
|
||||
## Consequences
|
||||
|
||||
- 数据模型需加 scenario version 字段,Run 需记录所用版本(v0.5 实施项,尚未实现)
|
||||
- 数据模型已加 scenario version 字段,Run 创建时快照所用版本(v0.5 已实现)
|
||||
- 动态用例每次运行题目不同不影响可比性——可比性单位是"同考纲"(同场景同版本),不是"同考卷"
|
||||
|
||||
@ -125,10 +125,11 @@ export default function ReportsPage() {
|
||||
const handleView = (runId: string) => {
|
||||
setSearchParams({ run: runId })
|
||||
loadReport(runId)
|
||||
// 对比报告要求同场景:A 变更后若 B 场景不同则清空
|
||||
// 对比报告要求同场景同版本:A 变更后若 B 不可比则清空
|
||||
const a = runs.find((r) => r.id === runId)
|
||||
const b = runs.find((r) => r.id === compareRunId)
|
||||
if (a && b && a.scenario_id !== b.scenario_id) setCompareRunId('')
|
||||
if (a && b && (a.scenario_id !== b.scenario_id
|
||||
|| (a.scenario_version ?? 1) !== (b.scenario_version ?? 1))) setCompareRunId('')
|
||||
}
|
||||
|
||||
const handleCompare = async () => {
|
||||
@ -205,10 +206,14 @@ export default function ReportsPage() {
|
||||
|
||||
const runSelectOptions = filteredRuns.map(buildOption)
|
||||
|
||||
// 报告 B 只能选与报告 A 同场景的 run
|
||||
// 报告 B 只能选与报告 A 同场景同版本(同考纲)的 run
|
||||
const selectedRun = runs.find((r) => r.id === selectedRunId)
|
||||
const compareOptions = runs
|
||||
.filter((r) => r.id !== selectedRunId && selectedRun && r.scenario_id === selectedRun.scenario_id)
|
||||
.filter((r) =>
|
||||
r.id !== selectedRunId
|
||||
&& selectedRun
|
||||
&& r.scenario_id === selectedRun.scenario_id
|
||||
&& (r.scenario_version ?? 1) === (selectedRun.scenario_version ?? 1))
|
||||
.map(buildOption)
|
||||
|
||||
const optionFilter = (input: string, opt?: { searchText?: string }) =>
|
||||
@ -291,7 +296,7 @@ export default function ReportsPage() {
|
||||
filterOption={optionFilter}
|
||||
options={compareOptions}
|
||||
popupMatchSelectWidth={false}
|
||||
notFoundContent={<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="没有同场景的其他评测记录" />}
|
||||
notFoundContent={<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="没有同场景同版本的其他评测记录" />}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Button
|
||||
|
||||
@ -39,7 +39,12 @@ def client_with_db(tmp_path):
|
||||
session.close()
|
||||
|
||||
|
||||
def _seed_run(session: Session, name_suffix: str = "", scenario_id: str | None = None) -> str:
|
||||
def _seed_run(
|
||||
session: Session,
|
||||
name_suffix: str = "",
|
||||
scenario_id: str | None = None,
|
||||
scenario_version: int = 1,
|
||||
) -> str:
|
||||
target = EvalTarget(
|
||||
name=f"target{name_suffix}",
|
||||
platform=PlatformType.AI_DIGITAL_EMPLOYEE,
|
||||
@ -60,6 +65,7 @@ def _seed_run(session: Session, name_suffix: str = "", scenario_id: str | None =
|
||||
run = EvalRun(
|
||||
target_id=target.id,
|
||||
scenario_id=scenario_id,
|
||||
scenario_version=scenario_version,
|
||||
status=RunStatus.COMPLETED,
|
||||
)
|
||||
run = RunRepository(session).create(run)
|
||||
@ -165,6 +171,29 @@ def test_compare_report_different_scenarios_400(client_with_db):
|
||||
assert "相同场景" in resp.json()["detail"]
|
||||
|
||||
|
||||
def test_compare_report_cross_version_400(client_with_db):
|
||||
"""同场景不同考纲版本 → 400,提示含双方版本号(ticket 05 / ADR-0001)。"""
|
||||
client, session = client_with_db
|
||||
run_id_a = _seed_run(session, "A")
|
||||
sid = RunRepository(session).get(run_id_a).scenario_id
|
||||
run_id_b = _seed_run(session, "B", scenario_id=sid, scenario_version=2)
|
||||
resp = client.get(f"/api/reports/compare?run1={run_id_a}&run2={run_id_b}")
|
||||
assert resp.status_code == 400
|
||||
detail = resp.json()["detail"]
|
||||
assert "v1" in detail and "v2" in detail
|
||||
|
||||
|
||||
def test_compare_report_same_version_ok(client_with_db):
|
||||
client, session = client_with_db
|
||||
run_id_a = _seed_run(session, "A", scenario_version=3)
|
||||
sid = RunRepository(session).get(run_id_a).scenario_id
|
||||
run_id_b = _seed_run(session, "B", scenario_id=sid, scenario_version=3)
|
||||
resp = client.get(f"/api/reports/compare?run1={run_id_a}&run2={run_id_b}")
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["run_a"]["scenario_version"] == 3
|
||||
assert resp.json()["run_b"]["scenario_version"] == 3
|
||||
|
||||
|
||||
def test_compare_report_run_not_found(client_with_db):
|
||||
client, session = client_with_db
|
||||
run_id = _seed_run(session)
|
||||
|
||||
@ -40,6 +40,7 @@ def _seed_run(
|
||||
pass_rate: float = 1.0,
|
||||
n_cases: int = 1,
|
||||
scenario_id: str | None = None,
|
||||
scenario_version: int = 1,
|
||||
connectivity_cases: int = 0,
|
||||
errored_cases: int = 0,
|
||||
) -> str:
|
||||
@ -72,6 +73,7 @@ def _seed_run(
|
||||
run = EvalRun(
|
||||
target_id=target.id,
|
||||
scenario_id=scenario_id,
|
||||
scenario_version=scenario_version,
|
||||
status=RunStatus.COMPLETED,
|
||||
)
|
||||
run = RunRepository(session).create(run)
|
||||
@ -242,6 +244,24 @@ def test_compare_report_different_scenarios_rejected(report_session):
|
||||
generate_compare_report(run_id_a, run_id_b, report_session)
|
||||
|
||||
|
||||
def test_compare_report_cross_version_rejected(report_session):
|
||||
"""同场景不同考纲版本不可比(ticket 05 / ADR-0001)。"""
|
||||
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, scenario_version=2)
|
||||
with pytest.raises(ValueError, match="version"):
|
||||
generate_compare_report(run_id_a, run_id_b, report_session)
|
||||
|
||||
|
||||
def test_compare_report_same_version_allowed(report_session):
|
||||
run_id_a = _seed_run(report_session, n_cases=1, scenario_version=2)
|
||||
sid = _scenario_of(report_session, run_id_a)
|
||||
run_id_b = _seed_run(report_session, n_cases=1, scenario_id=sid, scenario_version=2)
|
||||
result = generate_compare_report(run_id_a, run_id_b, report_session)
|
||||
assert result["run_a"]["scenario_version"] == 2
|
||||
assert result["run_b"]["scenario_version"] == 2
|
||||
|
||||
|
||||
# ── connectivity case annotation (ticket 02) ─────────────────────────────
|
||||
|
||||
def test_report_marks_connectivity_case(report_session):
|
||||
|
||||
Loading…
Reference in New Issue
Block a user