"""读标的修改类接口(FR-M2-07、08):采纳、修改、删除恢复、人工新增、补出处、确认清单、修改记录。""" from __future__ import annotations from typing import Any from tests.conftest import User, project_with_tender, run_parse async def _parsed(user: User) -> tuple[str, str, list[dict[str, Any]]]: project_id, doc_id = await project_with_tender(user, "tender-full.pdf") assert (await run_parse(user, doc_id))["status"] == "succeeded" items = (await user.get(f"/projects/{project_id}/checklist/items")).json()["items"] return project_id, doc_id, items def _pending_star(items: list[dict[str, Any]]) -> dict[str, Any]: return next(i for i in items if i["zone"] == "pending" and (i["requirement_text"] or "").startswith("*供应商")) async def test_accept_pending_with_changes_then_confirm(user: User) -> None: project_id, _, items = await _parsed(user) star = _pending_star(items) assert star["citations"] # 规则兜底的条目有出处,直接采纳即可 r = await user.post(f"/checklist-items/{star['id']}/accept", json={"changes": {"title": "须提供原厂授权书"}}) assert r.status_code == 200, r.text it = r.json() assert (it["zone"], it["pending_reason"], it["title"]) == ("formal", None, "须提供原厂授权书") assert it["edited_fields"] == ["title"] and it["accepted_by"]["name"] == "张工" assert it["ai_values"]["original"]["title"] == star["title"] again = await user.post(f"/checklist-items/{star['id']}/accept", json={}) assert again.status_code == 409 and again.json()["code"] == "item_not_pending" r = await user.post(f"/projects/{project_id}/checklist/confirm") assert r.status_code == 200, r.text cl = r.json() assert (cl["status"], cl["version_no"], cl["confirmed_by"]["name"]) == ("confirmed", 1, "张工") versions = (await user.get(f"/projects/{project_id}/checklist/versions")).json()["items"] assert [(v["version_no"], v["kind"], v["item_count"]) for v in versions] == [ (1, "confirmed", cl["counts"]["formal"]) ] assert (await user.get(f"/projects/{project_id}")).json()["next_action"] == "upload_bid" # 再改正式清单:回到草稿;改回原样再确认,复用 v1,不新增版本 formal = next(i for i in items if i["zone"] == "formal") r = await user.patch(f"/checklist-items/{formal['id']}", json={"title": "临时改一下"}) assert r.status_code == 200 and r.headers["ETag"] assert (await user.get(f"/projects/{project_id}/checklist")).json()["status"] == "draft" await user.patch(f"/checklist-items/{formal['id']}", json={"title": formal["title"]}) cl = (await user.post(f"/projects/{project_id}/checklist/confirm")).json() assert (cl["status"], cl["version_no"]) == ("confirmed", 1) async def test_accept_needs_citation_or_ack(user: User) -> None: project_id, doc_id, items = await _parsed(user) star = _pending_star(items) assert (await user.put(f"/checklist-items/{star['id']}/citations", json={"citation_ids": []})).status_code == 200 r = await user.post(f"/checklist-items/{star['id']}/accept", json={}) assert r.status_code == 422 and r.json()["code"] == "citation_required" # 在原文里选句子补出处,再采纳 page = star["citations"][0]["page_no"] sentences = (await user.get(f"/documents/{doc_id}/pages/{page}/sentences")).json()["items"] key = next(s["key"] for s in sentences if s["text"].startswith("*供应商")) r = await user.post(f"/projects/{project_id}/citations", json={"document_id": doc_id, "sentence_keys": [key]}) assert r.status_code == 201, r.text cite = r.json() assert cite["created_by_kind"] == "human" and cite["page_no"] == page and cite["quads"] bad = await user.post( f"/projects/{project_id}/citations", json={"document_id": doc_id, "sentence_keys": ["x:p1:s1"]} ) assert bad.status_code == 422 and bad.json()["code"] == "sentence_not_found" it = (await user.post(f"/checklist-items/{star['id']}/accept", json={"citation_ids": [cite["id"]]})).json() assert it["zone"] == "formal" and [c["id"] for c in it["citations"]] == [cite["id"]] assert "citations" in it["edited_fields"] and not it["no_citation_ack"] async def test_accept_without_citation_ack(user: User) -> None: _, _, items = await _parsed(user) star = _pending_star(items) await user.put(f"/checklist-items/{star['id']}/citations", json={"citation_ids": []}) it = (await user.post(f"/checklist-items/{star['id']}/accept", json={"no_citation_ack": True})).json() assert it["zone"] == "formal" and it["no_citation_ack"] and not it["citations"] async def test_discard_restore_and_reparse_keeps_decisions(user: User) -> None: project_id, doc_id, items = await _parsed(user) star = _pending_star(items) formal = next(i for i in items if i["zone"] == "formal") assert (await user.delete(f"/checklist-items/{star['id']}")).status_code == 204 await user.patch(f"/checklist-items/{formal['id']}", json={"title": "人工改过的标题"}) # 重新解析:丢弃的不再冒出来,人工改过的字段不被覆盖 assert (await run_parse(user, doc_id))["status"] == "succeeded" now = {i["id"]: i for i in (await user.get(f"/projects/{project_id}/checklist/items")).json()["items"]} assert star["id"] not in now and now[formal["id"]]["title"] == "人工改过的标题" counts = (await user.get(f"/projects/{project_id}/checklist")).json()["counts"] assert counts["deleted"] == 1 restored = (await user.post(f"/checklist-items/{star['id']}/restore")).json() assert restored["deleted_at"] is None and restored["zone"] == "pending" history = (await user.get(f"/checklist-items/{star['id']}/history")).json()["items"] assert [h["action"] for h in history] == [ "checklist_item.restore", "checklist_item.discard", "checklist_item.extract", ] assert history[-1]["actor"]["name"] == "系统" async def test_update_validation_and_precondition(user: User) -> None: _, _, items = await _parsed(user) formal = next(i for i in items if i["zone"] == "formal" and i["importance"] == "normal") r = await user.patch( f"/checklist-items/{formal['id']}", json={"importance": "substantive", "categories": ["scoring"]} ) assert r.status_code == 422 and r.json()["code"] == "validation_failed" r = await user.patch(f"/checklist-items/{formal['id']}", json={"title": None}) assert r.status_code == 422 r = await user.patch(f"/checklist-items/{formal['id']}", json={"title": "x"}, headers={"If-Match": '"0"'}) assert r.status_code == 412 and r.json()["code"] == "precondition_failed" etag = (await user.get(f"/checklist-items/{formal['id']}")).headers["ETag"] r = await user.patch( f"/checklist-items/{formal['id']}", json={"importance": "important"}, headers={"If-Match": etag} ) assert r.status_code == 200 and r.json()["importance_basis"] == "human" (h, *_) = (await user.get(f"/checklist-items/{formal['id']}/history")).json()["items"] assert h["action"] == "checklist_item.update" assert h["changes"] == [{"field": "importance", "before": "normal", "after": "important"}] assert h["actor"]["name"] == "张工" async def test_manual_item(user: User) -> None: project_id, _, _ = await _parsed(user) r = await user.post( f"/projects/{project_id}/checklist/items", json={"categories": ["format_doc"], "title": "投标函须加盖公章"} ) assert r.status_code == 201, r.text it = r.json() assert (it["origin"], it["origin_label"], it["zone"], it["citations"]) == ("human", "人工添加", "formal", []) filtered = (await user.get(f"/projects/{project_id}/checklist/items", params={"category": "format_doc"})).json() assert [i["id"] for i in filtered["items"]] == [it["id"]] (h,) = (await user.get(f"/checklist-items/{it['id']}/history")).json()["items"] assert h["action"] == "checklist_item.create" async def test_confirm_empty_checklist(user: User) -> None: project = (await user.post("/projects", json={})).json() r = await user.post(f"/projects/{project['id']}/checklist/confirm") assert r.status_code == 409 and r.json()["code"] == "checklist_empty"