feat(task): 批量改状态与看板数据接口

- POST /items/batch-status:逐条成败,不做全回滚;拖 10 条里 1 条冲突不该让另 9 条白干
- GET /items/board:四列一次取全,收件箱不进看板
- 修:/items/board 被 /{item_id} 抢先匹配导致 422
- 新增向量库配置项(M3 用,独立实例,不动 fnsvr-01)
- 3 个新测试,全量 46 passed
This commit is contained in:
dadao
2026-10-28 04:05:35 +08:00
parent e25a1a4e31
commit c4868f04cf
6 changed files with 329 additions and 1 deletions
+87
View File
@@ -270,3 +270,90 @@ async def test_cursor_pagination_does_not_repeat_rows(client: AsyncClient) -> No
assert ids_first.isdisjoint(ids_second)
assert len(ids_second) == 1
assert second["has_more"] is False
async def test_batch_status_partial_success(client: AsyncClient) -> None:
"""拖 3 条进去:2 条该成功,1 条因为版本过期该失败,且失败不影响另外两条。"""
user = await login(client)
ws = await create_workspace(client, user, "个人")
a = await _new_item(client, user, ws["id"], title="A", status="todo")
b = await _new_item(client, user, ws["id"], title="B", status="todo")
stale = await _new_item(client, user, ws["id"], title="C", status="todo")
# 让 C 的版本先过期:另一个人(同一用户)改了一次
unwrap(
await client.patch(
f"{items_url(ws['id'])}/{stale['id']}",
json={"title": "C 改过", "version": stale["version"]},
headers=user["headers"],
)
)
body = unwrap(
await client.post(
f"{items_url(ws['id'])}/batch-status",
json={
"status": "done",
"items": [
{"id": a["id"], "version": a["version"]},
{"id": b["id"], "version": b["version"]},
{"id": stale["id"], "version": stale["version"]},
],
},
headers=user["headers"],
)
)
assert len(body["updated"]) == 2
assert len(body["failed"]) == 1
assert body["failed"][0]["id"] == stale["id"]
assert body["failed"][0]["code"] == 4009
assert body["failed"][0]["current_version"] == stale["version"] + 1
detail_a = unwrap(await client.get(f"{items_url(ws['id'])}/{a['id']}", headers=user["headers"]))
assert detail_a["status"] == "done"
assert detail_a["completed_at"] is not None
async def test_batch_status_reports_illegal_transition(client: AsyncClient) -> None:
user = await login(client)
ws = await create_workspace(client, user, "个人")
inbox = await _new_item(client, user, ws["id"], title="还在收件箱")
body = unwrap(
await client.post(
f"{items_url(ws['id'])}/batch-status",
json={"status": "done", "items": [{"id": inbox["id"], "version": inbox["version"]}]},
headers=user["headers"],
)
)
assert body["updated"] == []
assert body["failed"][0]["code"] == 5002
assert "todo" in body["failed"][0]["allowed"]
async def test_board_groups_by_status_and_hides_others(client: AsyncClient) -> None:
user = await login(client)
ws = await create_workspace(client, user, "个人")
await _new_item(client, user, ws["id"], title="待办1", status="todo")
await _new_item(client, user, ws["id"], title="在做1", status="doing")
await _new_item(client, user, ws["id"], title="卡住1", status="blocked")
await _new_item(client, user, ws["id"], title="收件箱的", status="inbox")
board = unwrap(await client.get(f"{items_url(ws['id'])}/board", headers=user["headers"]))
columns = board["columns"]
assert board["order"] == ["todo", "doing", "blocked", "done"]
assert [i["title"] for i in columns["todo"]] == ["待办1"]
assert [i["title"] for i in columns["doing"]] == ["在做1"]
assert [i["title"] for i in columns["blocked"]] == ["卡住1"]
assert columns["done"] == []
# 收件箱不属于看板任何一列
all_titles = [i["title"] for col in columns.values() for i in col]
assert "收件箱的" not in all_titles
# 别人的工作区看板为空
other_user = await login(client)
other_ws = await create_workspace(client, other_user, "别人")
empty = unwrap(
await client.get(f"{items_url(other_ws['id'])}/board", headers=other_user["headers"])
)
assert all(len(col) == 0 for col in empty["columns"].values())