test_p2_wp13_acceptance_handover_contract.py 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. from __future__ import annotations
  2. import json
  3. import re
  4. from pathlib import Path
  5. ROOT = Path(__file__).resolve().parents[1]
  6. UAT_CASES = ROOT / "docs/acceptance/P2_WP13_UAT_CASES.json"
  7. UAT_REPORT = ROOT / "docs/acceptance/P2_WP13_UAT_PLAN_AND_REPORT.md"
  8. DEFECT_REGISTER = ROOT / "docs/acceptance/P2_WP13_DEFECT_REGISTER.md"
  9. SIGNOFF = ROOT / "docs/acceptance/P2_WP13_SIGNOFF_CHECKLIST.md"
  10. USER_GUIDE = ROOT / "docs/guides/DATAOPS_PHASE2_USER_OPERATION_GUIDE.md"
  11. OPERATIONS_HANDOVER = ROOT / "docs/runbooks/DATAOPS_PHASE2_OPERATIONS_HANDOVER.md"
  12. TRAINING = ROOT / "docs/training/DATAOPS_PHASE2_TRAINING_PLAN.md"
  13. PHASE3_BACKLOG = ROOT / "docs/roadmap/DATAOPS_PHASE3_BACKLOG.md"
  14. CENSUS = ROOT / "docs/FUNCTION_MODULE_CENSUS_20260726.md"
  15. CENSUS_AUDIT = ROOT / "docs/validation/P2_WP13_FUNCTION_CENSUS_AUDIT.md"
  16. EVIDENCE = ROOT / "docs/validation/P2_WP13_ACCEPTANCE_HANDOVER_EVIDENCE.md"
  17. PHASE2_PLAN = ROOT / "docs/DATAOPS_PHASE2_3_MONTH_DEVELOPMENT_PLAN_20260730.md"
  18. DELIVERABLES = (
  19. UAT_CASES,
  20. UAT_REPORT,
  21. DEFECT_REGISTER,
  22. SIGNOFF,
  23. USER_GUIDE,
  24. OPERATIONS_HANDOVER,
  25. TRAINING,
  26. PHASE3_BACKLOG,
  27. CENSUS_AUDIT,
  28. EVIDENCE,
  29. )
  30. P2_COMPLETED_LEDGER_IDS = {
  31. "GOV-05",
  32. "GOV-06",
  33. "GOV-07",
  34. "GOV-08",
  35. "GOV-09",
  36. "CAT-17",
  37. "CAT-23",
  38. "CAT-26",
  39. "OBS-04",
  40. "OBS-05",
  41. "OBS-06",
  42. "WFC-04",
  43. "WFC-05",
  44. "WFC-06",
  45. "WFC-07",
  46. "WFC-08",
  47. "WFC-09",
  48. "WFC-10",
  49. "WFC-11",
  50. "WFC-12",
  51. "WFC-15",
  52. }
  53. def _read(path: Path) -> str:
  54. return path.read_text(encoding="utf-8")
  55. def _cases() -> list[dict]:
  56. payload = json.loads(_read(UAT_CASES))
  57. assert payload["schema_version"] == "1.0"
  58. assert payload["work_package"] == "P2-WP13"
  59. return payload["cases"]
  60. def _ledger_rows() -> dict[str, list[str]]:
  61. rows: dict[str, list[str]] = {}
  62. for line in _read(CENSUS).splitlines():
  63. match = re.match(r"^\| ([A-Z]{3}-\d{2}) \| (.+) \| (.+) \| (.+) \|$", line)
  64. if match:
  65. rows[match.group(1)] = [match.group(2), match.group(3), match.group(4)]
  66. return rows
  67. def test_p2_wp13_deliverables_exist() -> None:
  68. missing = [str(path.relative_to(ROOT)) for path in DELIVERABLES if not path.is_file()]
  69. assert not missing, f"P2-WP13 交付物缺失: {missing}"
  70. def test_uat_cases_cover_all_work_packages_roles_and_external_boundaries() -> None:
  71. cases = _cases()
  72. assert len(cases) >= 24
  73. required_fields = {
  74. "id",
  75. "work_packages",
  76. "module_ids",
  77. "title",
  78. "actor",
  79. "acceptance_level",
  80. "preconditions",
  81. "steps",
  82. "expected",
  83. "evidence",
  84. "result",
  85. "notes",
  86. }
  87. actors = {"admin", "editor", "viewer", "second_domain_owner", "enterprise_acceptance"}
  88. for case in cases:
  89. assert required_fields <= case.keys(), case.get("id")
  90. assert case["id"].startswith("P2-WP13-UAT-")
  91. assert case["actor"] in actors
  92. assert case["acceptance_level"] in {"LOCAL_ENGINEERING", "ENTERPRISE_FORMAL"}
  93. assert case["result"] in {"PASS_LOCAL", "FAIL", "BLOCKED_EXTERNAL", "NOT_RUN"}
  94. assert case["work_packages"] and case["module_ids"]
  95. assert case["preconditions"] and len(case["steps"]) >= 2
  96. assert case["expected"] and case["evidence"]
  97. if case["acceptance_level"] == "ENTERPRISE_FORMAL":
  98. assert case["result"] != "PASS_LOCAL"
  99. covered = {work_package for case in cases for work_package in case["work_packages"]}
  100. assert {f"P2-WP{number:02d}" for number in range(1, 13)} <= covered
  101. assert actors - {"enterprise_acceptance"} <= {case["actor"] for case in cases}
  102. assert any(case["result"] == "BLOCKED_EXTERNAL" for case in cases)
  103. assert not any(case["result"] == "FAIL" for case in cases)
  104. def test_local_uat_evidence_references_existing_workspace_files() -> None:
  105. for case in _cases():
  106. if case["result"] != "PASS_LOCAL":
  107. continue
  108. for reference in case["evidence"]:
  109. path = ROOT / reference.split("#", 1)[0]
  110. assert path.is_file(), f"{case['id']} evidence missing: {reference}"
  111. def test_acceptance_docs_are_explicit_about_local_and_enterprise_status() -> None:
  112. report = _read(UAT_REPORT)
  113. for phrase in (
  114. "本地工程预验收",
  115. "企业正式验收",
  116. "不能替代企业真实数据 UAT",
  117. "BLOCKED_EXTERNAL",
  118. "P0",
  119. "签字",
  120. ):
  121. assert phrase in report
  122. for path in DELIVERABLES[1:]:
  123. assert not re.search(r"\b(?:TBD|TODO)\b|\{\{[^}]+\}\}", _read(path)), path.name
  124. def test_defect_register_and_signoff_are_fail_closed() -> None:
  125. defects = _read(DEFECT_REGISTER)
  126. for field in ("缺陷编号", "严重度", "状态", "复现与证据", "处置决定"):
  127. assert field in defects
  128. for severity in ("P0", "P1", "P2", "P3"):
  129. assert severity in defects
  130. assert "P0:0" in defects
  131. assert "未关闭 P1:0" in defects
  132. signoff = _read(SIGNOFF)
  133. for role in ("产品负责人", "业务负责人", "技术负责人", "安全负责人", "运维负责人"):
  134. assert role in signoff
  135. assert "不得预签" in signoff
  136. assert "BLOCKED_EXTERNAL" in signoff
  137. def test_user_operations_and_training_materials_cover_handover_scope() -> None:
  138. user_guide = _read(USER_GUIDE)
  139. for topic in (
  140. "管理员",
  141. "编辑者",
  142. "查看者",
  143. "第二业务域责任人",
  144. "主动元数据",
  145. "语义",
  146. "质量",
  147. "事故",
  148. "数据产品",
  149. "Agent",
  150. "安全",
  151. ):
  152. assert topic in user_guide
  153. operations = _read(OPERATIONS_HANDOVER)
  154. for topic in (
  155. "每日巡检",
  156. "每周巡检",
  157. "备份",
  158. "恢复",
  159. "回滚",
  160. "升级",
  161. "异常升级",
  162. "责任",
  163. "移交清单",
  164. ):
  165. assert topic in operations
  166. training = _read(TRAINING)
  167. for topic in ("用户培训", "运维培训", "练习", "签到", "补训", "BLOCKED_EXTERNAL"):
  168. assert topic in training
  169. def test_phase3_backlog_preserves_product_boundary_and_sso_gate() -> None:
  170. backlog = _read(PHASE3_BACKLOG)
  171. for field in ("待办编号", "来源模块", "优先级", "启动条件", "验收标准", "依赖", "目标阶段"):
  172. assert field in backlog
  173. for topic in ("单点登录", "OIDC", "IdP", "企业数据源", "多租户", "生产可观测", "第三业务域"):
  174. assert topic in backlog
  175. assert "不扩展为分析开发平台" in backlog
  176. def test_function_census_has_264_unique_audited_items_and_no_stale_p2_plans() -> None:
  177. rows = _ledger_rows()
  178. assert len(rows) == 264
  179. assert all(row[2].strip() for row in rows.values())
  180. for module_id in P2_COMPLETED_LEDGER_IDS:
  181. assert rows[module_id][2] != "规划中", module_id
  182. audit = _read(CENSUS_AUDIT)
  183. assert "264" in audit
  184. assert "264/264" in audit
  185. assert "生产已建设" not in audit
  186. assert "企业 UAT" in audit
  187. def test_phase2_plan_and_evidence_record_engineering_completion_without_formal_signoff() -> None:
  188. plan = _read(PHASE2_PLAN)
  189. section = plan.split("### P2-WP13 第二阶段验收与移交", 1)[1].split("## 8.", 1)[0]
  190. assert section.count("- [x]") == 5
  191. assert "本地工程完成" in section
  192. assert "企业正式验收未完成" in section
  193. evidence = _read(EVIDENCE)
  194. for section_name in (
  195. "文档契约",
  196. "UAT 覆盖",
  197. "角色与权限",
  198. "缺陷结论",
  199. "培训与移交",
  200. "功能台账",
  201. "外部门禁",
  202. ):
  203. assert section_name in evidence
  204. assert "tests/test_p2_wp13_acceptance_handover_contract.py" in evidence
  205. assert "PASS_LOCAL" in evidence
  206. assert "BLOCKED_EXTERNAL" in evidence