feat(api): 实验归档支持批量删除(含缺失项如实回报)

「实验对比」页需要批量删除,逐个 DELETE 会有 N 次往返且中途失败会留下半删除状态。

- `POST /experiments/bulk-delete`:一次最多 200 个 id(`BULK_DELETE_MAX_IDS`),
  按请求顺序去重;返回 `deleted` / `missing` / `count`,**存在的删掉、不存在的如实列出**,
  不假装全部成功(前端据此提示「N 个已删、M 个不存在」)。
- 路由声明在 `GET /{experiment_id}` 之前,避免被路径参数吞掉。
- 测试 +2:删除与缺失混合场景、id 列表校验(空/超长)。
This commit is contained in:
Simon
2026-10-01 17:57:04 +08:00
parent 48a97c2a12
commit 633176a3d1
2 changed files with 88 additions and 1 deletions
+49
View File
@@ -6,9 +6,11 @@ import sys
from datetime import date, datetime
import pytest
from app.api.experiments import BULK_DELETE_MAX_IDS
from app.application.services import job_executor as je
from app.application.services.job_executor import execute_job
from app.domain.entities.research import (
ExperimentRecord,
JobRecord,
JobStatus,
ResearchSpec,
@@ -266,6 +268,53 @@ class TestJobsApi:
with TestClient(app) as client:
assert client.get("/api/jobs/JOB-NOPE").status_code == 404
def test_bulk_delete_reports_deleted_and_missing(self, seeded_api_db) -> None:
"""批量删除:成功 / 库里没有的 id **分开回报**,且真的从列表里消失。
为什么坚持分开回报:把「没找到」也算成功,界面上就会显示「已删除 4 个」
而实际只删了 2 个 —— 用户下次刷新发现还在,却查不出原因。
"""
spec_json = _spec().model_dump_json()
with sess_mod.SessionLocal() as session:
repo = SqlAlchemyExperimentRepository(session)
for eid in ("EXP-A", "EXP-B"):
repo.save(
ExperimentRecord(
id=eid,
kind="backtest",
spec_json=spec_json,
result_json="{}",
summary_text="自检用",
created_at=datetime(2026, 10, 1, 10, 0, 0),
)
)
session.commit()
with TestClient(app) as client:
# 重复 id 只算一次(EXP-A 出现两次)
resp = client.post(
"/api/experiments/bulk-delete",
json={"ids": ["EXP-A", "EXP-B", "EXP-A", "EXP-NOPE"]},
)
assert resp.status_code == 200
body = resp.json()
assert body["deleted"] == ["EXP-A", "EXP-B"]
assert body["missing"] == ["EXP-NOPE"]
assert body["count"] == 2
left = {e["id"] for e in client.get("/api/experiments").json()}
assert not ({"EXP-A", "EXP-B"} & left)
def test_bulk_delete_validates_ids(self, seeded_api_db) -> None:
"""空列表 / 超上限 / 缺字段一律 422(不静默截断成前 N 个)。"""
with TestClient(app) as client:
assert client.post("/api/experiments/bulk-delete", json={"ids": []}).status_code == 422
assert client.post("/api/experiments/bulk-delete", json={}).status_code == 422
too_many = [f"EXP-{i}" for i in range(BULK_DELETE_MAX_IDS + 1)]
resp = client.post("/api/experiments/bulk-delete", json={"ids": too_many})
assert resp.status_code == 422
# 错误里必须写清上限,用户才知道要分几批
assert str(BULK_DELETE_MAX_IDS) in resp.text
def test_experiments_list_and_detail(self, seeded_api_db) -> None:
with TestClient(app) as client:
# 先跑一个 job 生成归档,再验证列表与详情