test_phase3_wp14_acceptance_handover_contract.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446
  1. from __future__ import annotations
  2. import hashlib
  3. import json
  4. import re
  5. import subprocess
  6. import sys
  7. from datetime import datetime, timezone
  8. from pathlib import Path
  9. ROOT = Path(__file__).resolve().parents[1]
  10. sys.path.insert(0, str(ROOT / "scripts"))
  11. from generate_p3_wp14_release_manifest import _tree_digest as _manifest_tree_digest # noqa: E402
  12. CASES = ROOT / "docs/acceptance/P3_WP14_UAT_CASES.json"
  13. REPORT = ROOT / "docs/acceptance/P3_WP14_UAT_PLAN_AND_REPORT.md"
  14. DEFECTS = ROOT / "docs/acceptance/P3_WP14_DEFECT_AND_GATE_LEDGER.md"
  15. SIGNOFF = ROOT / "docs/acceptance/P3_WP14_FIVE_PARTY_SIGNOFF.md"
  16. RUNBOOK = ROOT / "docs/runbooks/P3_WP14_ENTERPRISE_PILOT_HANDOVER.md"
  17. EVIDENCE = ROOT / "docs/validation/P3_WP14_ENTERPRISE_PILOT_EVIDENCE.md"
  18. RELEASE = ROOT / "docs/phase3/P3_WP14_RELEASE_TRACEABILITY.json"
  19. MANIFEST = ROOT / "docs/phase3/P3_WP14_RELEASE_MANIFEST.json"
  20. AUDIT = ROOT / "docs/phase3/P3_WP14_264_MODULE_CLOSURE_AUDIT.json"
  21. CONFIG_A = ROOT / "docs/validation/P3_WP14_LOCAL_CONFIG_A.json"
  22. CONFIG_B = ROOT / "docs/validation/P3_WP14_LOCAL_CONFIG_B.json"
  23. CONFIG_COMPARISON = ROOT / "docs/validation/P3_WP14_LOCAL_CONFIG_COMPARISON.json"
  24. CONFIG_RENDER = ROOT / "docs/validation/P3_WP14_LOCAL_CONFIG_RENDER.json"
  25. VERIFICATION_LEDGER = ROOT / "docs/validation/P3_WP14_LOCAL_VERIFICATION_LEDGER.json"
  26. VERIFICATION_LOG = ROOT / "docs/validation/P3_WP14_LOCAL_VERIFICATION.log"
  27. FRESH_VERIFIER = ROOT / "scripts/verify_p3_wp14_fresh_local_engineering.py"
  28. PHASE_PLAN = ROOT / "docs/DATAOPS_PHASE3_6_MONTH_DEVELOPMENT_PLAN_20260802.md"
  29. BACKLOG = ROOT / "docs/roadmap/DATAOPS_PHASE3_BACKLOG.md"
  30. LEDGER = ROOT / "docs/FUNCTION_MODULE_CENSUS_20260726.md"
  31. REQUIREMENTS = ROOT / "docs/phase3/P3_WP00_REQUIREMENTS.json"
  32. SOURCE_HEAD = ROOT / "migrations/versions/20260818_559_plugin_three_identity_gate.py"
  33. DEPLOYMENT_HEAD = ROOT / "deployment/migrations/versions/20260818_559_plugin_three_identity_gate.py"
  34. REQUIRED_DELIVERABLES = (
  35. CASES,
  36. REPORT,
  37. DEFECTS,
  38. SIGNOFF,
  39. RUNBOOK,
  40. EVIDENCE,
  41. RELEASE,
  42. AUDIT,
  43. MANIFEST,
  44. CONFIG_A,
  45. CONFIG_B,
  46. CONFIG_COMPARISON,
  47. CONFIG_RENDER,
  48. VERIFICATION_LEDGER,
  49. VERIFICATION_LOG,
  50. FRESH_VERIFIER,
  51. )
  52. STARTED_WORK_PACKAGES = {f"P3-WP{number:02d}" for number in range(1, 14)}
  53. REQUIRED_ROLES = {
  54. "admin",
  55. "governance",
  56. "ordinary_user",
  57. "data_owner",
  58. "operations",
  59. "security",
  60. "audit",
  61. }
  62. REQUIRED_CATEGORIES = {
  63. "permission",
  64. "interface",
  65. "database",
  66. "browser_ui",
  67. "enterprise_boundary",
  68. "migration",
  69. "upgrade",
  70. "rollback",
  71. "restore",
  72. "target_scale",
  73. "failure",
  74. "security",
  75. "training",
  76. "operations",
  77. "five_party_signoff",
  78. "isolated_local_environment",
  79. }
  80. def _read(path: Path) -> str:
  81. return path.read_text(encoding="utf-8")
  82. def _json(path: Path) -> dict:
  83. return json.loads(_read(path))
  84. def _sha256(path: Path) -> str:
  85. return hashlib.sha256(path.read_bytes()).hexdigest()
  86. def _tree_digest(relative_root: str) -> str:
  87. return _manifest_tree_digest(relative_root)
  88. def _ledger_rows() -> dict[str, dict[str, str]]:
  89. content = _read(LEDGER)
  90. section = content[content.index("## 12. 企业级完整产品功能模块台账") :]
  91. rows: dict[str, dict[str, str]] = {}
  92. for line in section.splitlines():
  93. cells = [cell.strip() for cell in line.strip().strip("|").split("|")]
  94. if len(cells) == 4 and re.fullmatch(r"[A-Z]{3}-\d{2}", cells[0]):
  95. module_id, module_name, description, maturity = cells
  96. rows[module_id] = {
  97. "module_name": module_name,
  98. "description": description,
  99. "maturity": maturity,
  100. }
  101. return rows
  102. def test_p3_wp14_deliverables_exist_and_external_boundary_is_closed() -> None:
  103. missing = [str(path.relative_to(ROOT)) for path in REQUIRED_DELIVERABLES if not path.is_file()]
  104. assert not missing, f"P3-WP14 delivery missing: {missing}"
  105. prohibited = ("PASS_ENTERPRISE", "ENTERPRISE_PASS", "ENTERPRISE_ACCEPTANCE_COMPLETE")
  106. local_artifacts = {
  107. CONFIG_A,
  108. CONFIG_B,
  109. CONFIG_COMPARISON,
  110. CONFIG_RENDER,
  111. VERIFICATION_LEDGER,
  112. VERIFICATION_LOG,
  113. MANIFEST,
  114. FRESH_VERIFIER,
  115. }
  116. for path in REQUIRED_DELIVERABLES:
  117. assert not any(token in _read(path) for token in prohibited), path
  118. assert "TBD_EXTERNAL" in _read(path) or path == AUDIT or path in local_artifacts, path
  119. def test_p3_wp14_machine_matrix_covers_started_scope_roles_and_boundaries() -> None:
  120. payload = _json(CASES)
  121. assert payload["schema_version"] == "1.0"
  122. assert payload["work_package"] == "P3-WP14"
  123. assert payload["result_definitions"]["PASS_LOCAL"]
  124. cases = payload["cases"]
  125. assert len(cases) >= 29
  126. required_fields = {
  127. "id",
  128. "work_packages",
  129. "module_ids",
  130. "title",
  131. "actor",
  132. "categories",
  133. "acceptance_level",
  134. "preconditions",
  135. "steps",
  136. "expected",
  137. "evidence",
  138. "result",
  139. "notes",
  140. }
  141. assert len({case["id"] for case in cases}) == len(cases)
  142. for case in cases:
  143. assert required_fields <= case.keys(), case.get("id")
  144. assert case["id"].startswith("P3-WP14-UAT-")
  145. assert set(case["work_packages"]) <= STARTED_WORK_PACKAGES
  146. assert case["module_ids"]
  147. assert case["actor"] in REQUIRED_ROLES
  148. assert case["categories"]
  149. assert case["acceptance_level"] in {"LOCAL_ENGINEERING", "ENTERPRISE_FORMAL"}
  150. assert case["result"] in {"PASS_LOCAL", "FAIL", "BLOCKED_EXTERNAL", "NOT_RUN", "TBD_EXTERNAL"}
  151. assert case["preconditions"] and case["steps"] and case["expected"] and case["evidence"]
  152. if case["acceptance_level"] == "ENTERPRISE_FORMAL":
  153. assert case["result"] in {"BLOCKED_EXTERNAL", "TBD_EXTERNAL", "NOT_RUN", "FAIL"}
  154. if case["result"] == "PASS_LOCAL":
  155. for reference in case["evidence"]:
  156. assert (ROOT / reference.split("#", 1)[0]).is_file(), (case["id"], reference)
  157. assert {wp for case in cases for wp in case["work_packages"]} == STARTED_WORK_PACKAGES
  158. assert {case["actor"] for case in cases} >= REQUIRED_ROLES
  159. assert {category for case in cases for category in case["categories"]} >= REQUIRED_CATEGORIES
  160. assert {wp for case in cases if case["result"] == "PASS_LOCAL" for wp in case["work_packages"]} == STARTED_WORK_PACKAGES
  161. assert any(case["result"] == "BLOCKED_EXTERNAL" for case in cases)
  162. assert not any(case["result"] == "FAIL" for case in cases)
  163. by_id = {case["id"]: case for case in cases}
  164. assert by_id["P3-WP14-UAT-22"]["acceptance_level"] == "ENTERPRISE_FORMAL"
  165. assert by_id["P3-WP14-UAT-22"]["result"] == "BLOCKED_EXTERNAL"
  166. assert by_id["P3-WP14-UAT-30"]["acceptance_level"] == "LOCAL_ENGINEERING"
  167. assert by_id["P3-WP14-UAT-30"]["result"] == "PASS_LOCAL"
  168. def test_p3_wp14_release_traceability_is_tamper_evident_and_uses_shared_head() -> None:
  169. release = _json(RELEASE)
  170. assert release["schema_version"] == "1.0"
  171. assert release["work_package"] == "P3-WP14"
  172. assert release["release_status"] in {
  173. "IN_PROGRESS",
  174. "ENGINEERING_BASELINE_COMPLETE_ENTERPRISE_UAT_BLOCKED",
  175. }
  176. assert release["migration_head"] == "20260818_559"
  177. assert release["signature_status"] == "TBD_EXTERNAL"
  178. assert release["remote_save_status"] == "TBD_EXTERNAL"
  179. assert release["release_manifest_path"] == "docs/phase3/P3_WP14_RELEASE_MANIFEST.json"
  180. assert release["release_manifest_sha256"] == _sha256(MANIFEST)
  181. assert release["verification_ledger_sha256"] == _sha256(VERIFICATION_LEDGER)
  182. assert release["verification_log_sha256"] == _sha256(VERIFICATION_LOG)
  183. assert SOURCE_HEAD.read_bytes() == DEPLOYMENT_HEAD.read_bytes()
  184. assert 'revision = "20260818_559"' in _read(SOURCE_HEAD)
  185. assert 'down_revision = "20260818_558"' in _read(SOURCE_HEAD)
  186. manifest = _json(MANIFEST)
  187. assert manifest["schema_version"] == "1.0"
  188. assert manifest["release_identity"]["state"] == "UNCOMMITTED_WORKTREE_EVIDENCE"
  189. assert manifest["release_identity"]["signed"] is False
  190. assert manifest["release_identity"]["git_base_commit"] == subprocess.check_output(
  191. ["git", "rev-parse", "HEAD"], cwd=ROOT, text=True
  192. ).strip()
  193. assert manifest["release_identity"]["git_base_tree"] == subprocess.check_output(
  194. ["git", "rev-parse", "HEAD^{tree}"], cwd=ROOT, text=True
  195. ).strip()
  196. assert manifest["release_identity"]["worktree_clean"] is False
  197. expected_roots = {"app", "deployment/app", "migrations", "deployment/migrations"}
  198. tree_sets = {
  199. item["root"]: item
  200. for item in manifest["controlled_artifact_sets"]
  201. if item["kind"] == "tree"
  202. }
  203. assert expected_roots <= set(tree_sets)
  204. for root, item in tree_sets.items():
  205. assert item["sha256"] == _tree_digest(root), root
  206. file_sets = {
  207. item["path"]: item["sha256"]
  208. for item in manifest["controlled_artifact_sets"]
  209. if item["kind"] == "file"
  210. }
  211. for path in (
  212. "docs/architecture/OPENAPI.yaml",
  213. "deploy/docker/docker-compose.yml",
  214. "scripts/generate_openapi.py",
  215. "docs/acceptance/P3_WP14_UAT_CASES.json",
  216. "tests/test_phase3_wp14_acceptance_handover_contract.py",
  217. ):
  218. assert file_sets[path] == _sha256(ROOT / path)
  219. def test_p3_wp14_isolated_configuration_and_fresh_verification_ledger_are_closed() -> None:
  220. snapshots = [_json(CONFIG_A), _json(CONFIG_B)]
  221. comparison = _json(CONFIG_COMPARISON)
  222. assert comparison["schema_version"] == "1.0"
  223. assert comparison["snapshot_paths"] == [
  224. "docs/validation/P3_WP14_LOCAL_CONFIG_A.json",
  225. "docs/validation/P3_WP14_LOCAL_CONFIG_B.json",
  226. ]
  227. assert comparison["allowed_difference_paths"] == [
  228. "/environment/label",
  229. "/runtime/DATAOPS_LOCAL_ENV_LABEL",
  230. "/runtime/BACKEND_PORT",
  231. ]
  232. assert len({snapshot["environment"]["label"] for snapshot in snapshots}) == 2
  233. for path, snapshot in zip((CONFIG_A, CONFIG_B), snapshots):
  234. assert set(snapshot) == {"schema_version", "environment", "runtime", "compose"}
  235. assert snapshot["schema_version"] == "1.0"
  236. assert set(snapshot["environment"]) == {"label", "is_enterprise"}
  237. assert snapshot["environment"]["is_enterprise"] is False
  238. assert set(snapshot["runtime"]) == {"DATAOPS_LOCAL_ENV_LABEL", "BACKEND_PORT"}
  239. assert set(snapshot["compose"]) == {"file", "render_command"}
  240. assert _sha256(path) == comparison["snapshot_sha256"][path.relative_to(ROOT).as_posix()]
  241. render = _json(CONFIG_RENDER)
  242. assert render["schema_version"] == "1.0"
  243. assert render["exit_code"] == 0
  244. assert render["snapshot_sha256"] == comparison["snapshot_sha256"]
  245. assert set(render["allowed_difference_paths"]) == set(comparison["allowed_difference_paths"])
  246. assert render["render_command"] == ["/usr/local/bin/docker-compose", "--env-file", "/dev/null", "-f", "deploy/docker/docker-compose.yml", "config", "--format", "json"]
  247. renders = render["renders"]
  248. assert set(renders) == {"LOCAL_ISOLATED_A", "LOCAL_ISOLATED_B"}
  249. service_sets = {tuple(item["services"]) for item in renders.values()}
  250. assert len(service_sets) == 1
  251. for snapshot, label in zip(snapshots, ("LOCAL_ISOLATED_A", "LOCAL_ISOLATED_B")):
  252. item = renders[label]
  253. assert item["exit_code"] == 0
  254. assert item["status"] == "PASS"
  255. assert item["raw_stdout_sha256"]
  256. assert item["canonical_stdout_sha256"]
  257. assert item["consumed_label"] == snapshot["runtime"]["DATAOPS_LOCAL_ENV_LABEL"]
  258. assert item["consumed_backend_port"] == snapshot["runtime"]["BACKEND_PORT"]
  259. release = _json(RELEASE)
  260. second_environment = release["second_isolated_local_environment"]
  261. assert second_environment["formal_same_signed_version_status"] == "BLOCKED_EXTERNAL"
  262. rehearsal = second_environment["local_portability_rehearsal"]
  263. assert rehearsal["result"] == "PASS_LOCAL"
  264. assert rehearsal["manifest_signed"] is False
  265. assert rehearsal["manifest_sha256"] == release["release_manifest_sha256"]
  266. assert "not the required same signed-version" in rehearsal["boundary"]
  267. ledger = _json(VERIFICATION_LEDGER)
  268. assert ledger["schema_version"] == "1.0"
  269. assert ledger["cache_class"] == "UNSIGNED_REPRODUCIBILITY_CACHE"
  270. assert ledger["pass_local_authority"] == "FRESH_VERIFIER_CURRENT_PROCESS_ONLY"
  271. assert ledger["external_signature_anchor"] == {
  272. "status": "TBD_EXTERNAL",
  273. "gate": "BLOCKED_EXTERNAL",
  274. }
  275. assert _sha256(VERIFICATION_LOG) == ledger["log_sha256"]
  276. controlled = {
  277. path: _sha256(ROOT / path) for path in ledger["controlled_input_paths"]
  278. }
  279. digest = hashlib.sha256(
  280. json.dumps(controlled, sort_keys=True, separators=(",", ":")).encode("utf-8")
  281. ).hexdigest()
  282. assert ledger["controlled_input_sha256"] == digest
  283. pass_ids = {
  284. case["id"] for case in _json(CASES)["cases"] if case["result"] == "PASS_LOCAL"
  285. }
  286. run_case_ids = {case_id for run in ledger["runs"] for case_id in run["case_ids"]}
  287. assert run_case_ids == pass_ids
  288. for run in ledger["runs"]:
  289. assert run["command_id"] and run["command"]
  290. assert run["status"] == "PASS"
  291. assert run["exit_code"] == 0
  292. assert run["passed_count"] > 0
  293. assert run["run_at"].endswith("Z")
  294. assert run["stdout_sha256"]
  295. assert run["stderr_sha256"]
  296. def test_p3_wp14_release_closure_and_ledger_security_envelope_are_fail_closed() -> None:
  297. manifest = _json(MANIFEST)
  298. closure = manifest["compose_input_closure"]
  299. assert closure["compose_file"] == "deploy/docker/docker-compose.yml"
  300. assert closure["compose_sha256"] == _sha256(ROOT / closure["compose_file"])
  301. assert closure["render_command"][-2:] == ["--format", "json"]
  302. assert closure["render_command"][1:3] == ["--env-file", "/dev/null"]
  303. assert closure["dockerignore"] == {"path": ".dockerignore", "sha256": _sha256(ROOT / ".dockerignore")}
  304. kinds = {item["kind"] for item in closure["inputs"]}
  305. assert {"build_context", "dockerfile", "requirements", "mount_input"} <= kinds
  306. for item in closure["inputs"]:
  307. path = ROOT / item["path"]
  308. assert path.exists()
  309. assert item["sha256"] == (_tree_digest(item["path"]) if path.is_dir() else _sha256(path))
  310. images = manifest["image_reference_closure"]
  311. assert images["immutable_signed_deployability_status"] == "BLOCKED_EXTERNAL"
  312. assert images["items"]
  313. for item in images["items"]:
  314. assert item["status"] in {"IMMUTABLE_DIGEST", "UNRESOLVED_MUTABLE_TAG", "INTERNAL_BUILD_STAGE"}
  315. if item["status"] == "UNRESOLVED_MUTABLE_TAG":
  316. assert item["gate"] == "BLOCKED_EXTERNAL"
  317. ledger = _json(VERIFICATION_LEDGER)
  318. state = _json(ROOT / "docs/validation/P3_WP14_VERIFICATION_RUN_STATE.json")
  319. assert state["status"] == "COMPLETE"
  320. assert ledger["run_nonce"] == state["run_nonce"]
  321. assert ledger["expires_at"] > ledger["generated_at"]
  322. assert datetime.fromisoformat(ledger["expires_at"].replace("Z", "+00:00")) > datetime.now(timezone.utc)
  323. assert ledger["security_envelope"] == {
  324. "locked": True,
  325. "atomic_write": True,
  326. "fd_single_read": True,
  327. "minimal_environment": True,
  328. "stdout_stderr_persisted": "SANITIZED_DIGEST_ONLY",
  329. }
  330. assert _sha256(FRESH_VERIFIER)
  331. for run in ledger["runs"]:
  332. assert run["timeout_seconds"] > 0
  333. assert run["cwd"] == "."
  334. assert run["command_sha256"] == hashlib.sha256(
  335. json.dumps(run["command"], separators=(",", ":")).encode("utf-8")
  336. ).hexdigest()
  337. assert run["script_sha256"]
  338. assert run["entry_path"] in run["dependency_sha256"]
  339. assert run["script_sha256"] == _sha256(ROOT / run["entry_path"])
  340. assert run["dependency_sha256"]
  341. aggregate = hashlib.sha256(json.dumps(run["dependency_sha256"], sort_keys=True, separators=(",", ":")).encode("utf-8")).hexdigest()
  342. assert run["dependency_aggregate_sha256"] == aggregate
  343. for path, digest in run["dependency_sha256"].items():
  344. assert _sha256(ROOT / path) == digest
  345. release = _json(RELEASE)
  346. assert release["local_verification_cache"] == {
  347. "classification": "UNSIGNED_REPRODUCIBILITY_CACHE",
  348. "pass_local_authority": "FRESH_VERIFIER_CURRENT_PROCESS_ONLY",
  349. "external_signature_status": "TBD_EXTERNAL",
  350. "enterprise_gate": "BLOCKED_EXTERNAL",
  351. }
  352. def test_p3_wp14_264_item_closure_audit_reads_source_ledger_without_promotion() -> None:
  353. audit = _json(AUDIT)
  354. rows = _ledger_rows()
  355. assert len(rows) == 264
  356. assert audit["schema_version"] == "1.0"
  357. assert audit["source_ledger"] == "docs/FUNCTION_MODULE_CENSUS_20260726.md#12"
  358. entries = audit["module_traceability"]
  359. assert len(entries) == 264
  360. by_id = {entry["module_id"]: entry for entry in entries}
  361. assert set(by_id) == set(rows)
  362. requirements = {
  363. item["module_id"]: item
  364. for item in _json(REQUIREMENTS)["module_traceability"]
  365. }
  366. for module_id, source in rows.items():
  367. entry = by_id[module_id]
  368. assert entry["module_name"] == source["module_name"]
  369. assert entry["description"] == source["description"]
  370. assert entry["source_maturity"] == source["maturity"]
  371. assert entry["p3_wp00_disposition"] == requirements[module_id]["disposition"]
  372. assert entry["implementation_owner_wp"] == requirements[module_id]["implementation_owner_wp"]
  373. assert entry["closure_wp"] == requirements[module_id]["closure_wp"]
  374. assert entry["enterprise_status"] in {"TBD_EXTERNAL", "BLOCKED_EXTERNAL", "NOT_APPLICABLE"}
  375. assert entry["evidence_refs"]
  376. def test_p3_wp14_report_ledger_signoff_runbook_and_status_sync_are_fail_closed() -> None:
  377. report = _read(REPORT)
  378. for phrase in (
  379. "本地工程验收",
  380. "企业正式 UAT",
  381. "不能替代",
  382. "P0",
  383. "P1",
  384. "TBD_EXTERNAL",
  385. "BLOCKED_EXTERNAL",
  386. "20260818_559",
  387. ):
  388. assert phrase in report
  389. defects = _read(DEFECTS)
  390. for phrase in ("P0:0", "未关闭 P1:0", "正式决定", "TBD_EXTERNAL", "BLOCKED_EXTERNAL"):
  391. assert phrase in defects
  392. signoff = _read(SIGNOFF)
  393. for role in ("产品", "业务", "技术", "安全", "运维"):
  394. assert role in signoff
  395. assert "不得预签" in signoff
  396. runbook = _read(RUNBOOK)
  397. for phrase in ("升级", "回滚", "恢复", "每日巡检", "每周巡检", "培训", "远端保存建议"):
  398. assert phrase in runbook
  399. plan = _read(PHASE_PLAN)
  400. section = plan.split("### P3-WP14 企业试点验收、移交与台账更新", 1)[1].split("## 8.", 1)[0]
  401. assert (
  402. "ENGINEERING_BASELINE_COMPLETE_ENTERPRISE_UAT_BLOCKED" in section
  403. or "IN_PROGRESS" in section
  404. )
  405. assert "企业正式 UAT" in section
  406. assert "TBD_EXTERNAL" in section
  407. backlog = _read(BACKLOG)
  408. assert "P3-WP14" in backlog
  409. assert "P3_WP14_264_MODULE_CLOSURE_AUDIT.json" in backlog