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:
@@ -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 生成归档,再验证列表与详情
|
||||
|
||||
Reference in New Issue
Block a user