"""Body<->appendix sync for the software-engineering skill (C-line). Scope guard: this file READS cloud/skills/software-engineering/SKILL.md and asserts structure; it never edits the skill text. If any assertion here fails, the fix is a skill-content decision (edit SKILL.md through its own track), not a silent edit by this test. Audit finding on origin/main (2026-10-31): there was NO bidirectional test at all. The only appendix coverage was test_skill_seed_worker.py asserting two chapters (00-coordinator, 08-shared-core) exist in the seeded file -- a dropped middle chapter (01..07) would pass CI silently. The bidirectional assertion previously discussed on the #209 track is unmerged, so main does not have it; this file supplies it on the main base (skill text v0.2, 9 appendix chapters 00..08; integrated text is v0.5, 11 appendix chapters 00..10). What "bidirectional" means here, concretely (all derived from SKILL.md itself, no new semantics invented): - body names the appendix as the authority AND the appendix intro claims itself as the authority (either side missing -> fail naming the missing side); - body names exactly three appendix chapters for the GitHub guarantee (coordinator, client, acceptance) AND each named chapter carries the rule keywords (each chapter's keyword set is tailored to what that chapter actually says -- e.g. 07-acceptance carries force-push / merge-gate / PR-number rules but no literal "GitHub" word, so the test must not demand that word there); - body role-matrix role names == appendix role-title names as sets (order differs: matrix order vs numeric 00..10 order, so set, not list, comparison), and appendix ids are exactly 00..10 in order. CI: .github/workflows/host-verify.yml and release.yml both run `python3 -m unittest discover -s cloud/tests -v`, so this test_*.py file runs in CI automatically with no workflow edit. Only software-engineering has an appendix (grep: the other four skills contain zero "附录"); appendix checks are therefore scoped to this one skill and must NOT be generalised to the other skills. """ import re import sys import tempfile import unittest from pathlib import Path try: from cloud.skill_seed import ensure_codex_home_skills except ImportError: sys.path.insert(0, str(Path(__file__).resolve().parents[2])) from cloud.skill_seed import ensure_codex_home_skills REPO_ROOT = Path(__file__).resolve().parents[2] SKILL_PATH = (REPO_ROOT / "cloud" / "skills" / "software-engineering" / "SKILL.md") EXPECTED_IDS = ["00-coordinator", "01-product-manager", "02-architect", "03-designer", "04-client-developer", "05-backend", "06-qa", "07-acceptance", "08-shared-core", "09-ops", "10-iteration", "11-auditor"] # Chapter -> keywords that chapter must carry for the GitHub guarantee # the body claims is "同样写进了附录的协调员、客户端、验收章节". # 07 has no literal "GitHub" word (its rules live under # "按端发布与回滚(验收)"); demanding it would be a false failure. GITHUB_CHAPTER_KEYWORDS = { "00-coordinator": ["GitHub 保证", "force-push", "合并门"], "04-client-developer": ["GitHub 交付", "force-push", "PR 号"], "07-acceptance": ["force-push", "合并门", "PR 号"], } CHAPTER_RE = re.compile( r"^## 附录角色:(\d{2}-[a-z0-9-]+)" r"(来源 templates/roles/(\d{2}-[a-z0-9-]+\.md))", flags=re.M) TITLE_RE = re.compile(r"^# 角色模板:(.+)$", flags=re.M) def _read_skill(): assert SKILL_PATH.is_file(), ( "正文侧缺失:技能源文件不存在:%s" % SKILL_PATH) return SKILL_PATH.read_text(encoding="utf-8") def _split(text): marker = "## 附录:角色模板" assert marker in text, "正文侧缺失:找不到附录引言节 %r" % marker body, appendix = text.split(marker, 1) return body, appendix def _chapters(appendix): matches = list(CHAPTER_RE.finditer(appendix)) chapters = {} for i, m in enumerate(matches): end = matches[i + 1].start() if i + 1 < len(matches) else len(appendix) chapters[m.group(1)] = appendix[m.start():end] return chapters, matches def _base_role_name(title): # "后端 / API(可选,1 个线程)" -> "后端 / API"; # "后端 / API(如有)" (matrix cell) -> "后端 / API". return re.sub(r"([^)]*)\s*$", "", title.strip()) class AppendixSyncTestCase(unittest.TestCase): def test_appendix_has_all_nine_chapters_in_order(self): text = _read_skill() _body, appendix = _split(text) chapters, matches = _chapters(appendix) found = [m.group(1) for m in matches] self.assertEqual( found, EXPECTED_IDS, "附录侧缺失/错序:附录章节 id 应为 %s,实际为 %s" % (EXPECTED_IDS, found)) for cid in EXPECTED_IDS: sec = chapters[cid] src = [m.group(2) for m in matches if m.group(1) == cid][0] self.assertEqual( src, cid + ".md", "附录侧不一致:章节 %s 的来源注应为 %s,实际为 %s" % (cid, cid + ".md", src)) title = TITLE_RE.search(sec) self.assertIsNotNone( title, "附录侧缺失:章节 %s 内找不到“# 角色模板:”标题行" % cid) # Smallest real chapter is ~600 chars; 300 catches a # dropped/emptied chapter without being brittle. self.assertGreater( len(sec), 300, "附录侧缺失/被掏空:章节 %s 仅 %d 字" % (cid, len(sec))) def test_body_role_matrix_matches_appendix_titles(self): text = _read_skill() body, appendix = _split(text) chapters, _m = _chapters(appendix) appendix_roles = set() for cid, sec in chapters.items(): title = TITLE_RE.search(sec) self.assertIsNotNone(title, "附录侧缺失:%s 无标题" % cid) appendix_roles.add(_base_role_name(title.group(1))) matrix_roles = set() # Only the role-matrix section: other body tables (gate table # with G0..G6, etc.) must not leak into the role set. matrix_section = body.split("## 角色线程矩阵", 1)[1] matrix_section = matrix_section.split("\n## ", 1)[0] for line in matrix_section.splitlines(): if not line.startswith("| "): continue cells = [c.strip() for c in line.strip().strip("|").split("|")] if not cells or cells[0] in ("角色", "------"): continue matrix_roles.add(_base_role_name(cells[0])) self.assertEqual( matrix_roles, appendix_roles, "双向不一致:正文角色矩阵缺 %s;附录缺角色 %s" % (sorted(appendix_roles - matrix_roles), sorted(matrix_roles - appendix_roles))) def test_authority_statement_is_bidirectional(self): text = _read_skill() body, appendix = _split(text) for phrase in ("角色模板的权威来源是本文件末尾的附录", "不一致时以附录为准"): self.assertIn( phrase, body, "正文侧缺失:正文未声明“%s”(附录单方面有权威也无法核对)" % phrase) intro = appendix.split("## 附录角色:", 1)[0] for phrase in ("以本附录为准", "核对附录与 `templates/roles/` 一致"): self.assertIn( phrase, intro, "附录侧缺失:附录引言未声明“%s”(正文单方面指认附录也无法核对)" % phrase) def test_github_guarantee_in_body_and_named_appendix_chapters(self): text = _read_skill() body, appendix = _split(text) chapters, _m = _chapters(appendix) for phrase in ("同样写进了附录的协调员、客户端、验收章节", "正文与附录不一致就是不合格"): self.assertIn( phrase, body, "正文侧缺失:正文 GitHub 保证节未声明“%s”" % phrase) for cid, keywords in GITHUB_CHAPTER_KEYWORDS.items(): self.assertIn( cid, chapters, "附录侧缺失:正文点名的章节 %s 不在附录中" % cid) for kw in keywords: self.assertIn( kw, chapters[cid], "附录侧缺失:章节 %s 缺少规则关键词 %r" "(正文 GitHub 保证节声称该规则已写进本章)" % (cid, kw)) def test_seeded_copy_carries_full_appendix_byte_identical(self): # Delivery-channel check: the seeded tenant copy must carry # the whole appendix, not just the two chapters the old test # checked. Byte-identity is the strongest form: skill_seed # copies SKILL.md verbatim, so any appendix edit in the pack # reaches the tenant only through this exact file. with tempfile.TemporaryDirectory() as tmp: out = ensure_codex_home_skills(tmp) self.assertIn("software-engineering", out["skills_copied"]) seeded = Path(tmp) / "skills" / "software-engineering" \ / "SKILL.md" self.assertTrue(seeded.is_file(), "播种侧缺失:未生成租户 SKILL.md") seeded_text = seeded.read_text(encoding="utf-8") self.assertEqual( seeded_text, _read_skill(), "播种侧不一致:租户 SKILL.md 与源包字节不一致(附录可能被截断)") _body, appendix = _split(seeded_text) chapters, _m = _chapters(appendix) self.assertEqual( sorted(chapters), sorted(EXPECTED_IDS), "播种侧缺失:租户副本附录章节不全:%s" % sorted(chapters)) if __name__ == "__main__": unittest.main()