"""Appendix provenance for the software-engineering skill. Background (see cloud/skills/software-engineering/CHANGELOG.md): SKILL.md claimed its appendix was "spliced verbatim from templates/roles/ by a generation script, see CHANGELOG", but the pack contained none of the three: no templates/, no CHANGELOG.md, no script. The appendix was therefore unverifiable -- a drifted chapter would pass CI silently. This file pins the repair, and nothing else: - tools/check_appendix.py passes on the real pack (all twelve chapters match their templates/roles/ sources, CHANGELOG present and mentioning templates/roles/); - the same script FAILS (nonzero exit) on a temp copy with a source deleted, a source tampered, or an extra source added -- so a future "source missing" or "appendix drifted" state cannot slip through; - --emit output is exactly the eleven headings plus the twelve source bodies, so the spliced form is itself derived from the sources. Scope guard: these tests never edit SKILL.md or the sources. A failure here is an editorial decision for the skill's own track (edit the source, then re-issue the appendix), not a silent rewrite. Seeding behaviour is pinned elsewhere (test_skill_seed_pinning.py): templates/, tools/ and CHANGELOG.md never reach the tenant home, because skill_seed copies SKILL.md only. """ import shutil import subprocess import sys import tempfile import unittest from pathlib import Path REPO_ROOT = Path(__file__).resolve().parents[2] SKILL_DIR = (REPO_ROOT / "cloud" / "skills" / "software-engineering") SCRIPT = SKILL_DIR / "tools" / "check_appendix.py" 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"] def _run(skill_dir=None): cmd = [sys.executable, str(SCRIPT)] if skill_dir is not None: cmd += ["--skill-dir", str(skill_dir)] return subprocess.run(cmd, capture_output=True, text=True) def _temp_skill_copy(testcase): tmp = tempfile.TemporaryDirectory() testcase.addCleanup(tmp.cleanup) dest = Path(tmp.name) / "software-engineering" shutil.copytree(SKILL_DIR, dest) return dest class AppendixProvenanceTestCase(unittest.TestCase): def test_check_script_passes_on_real_pack(self): self.assertTrue(SCRIPT.is_file(), "校验脚本缺失:%s" % SCRIPT) self.assertTrue( (SKILL_DIR / "CHANGELOG.md").is_file(), "CHANGELOG.md 缺失,SKILL.md 的“见 CHANGELOG”仍是悬空引用") result = _run() self.assertEqual( result.returncode, 0, "校验脚本应在真实包上通过,stderr:%s" % result.stderr) self.assertIn("12 chapters match", result.stdout) def test_missing_source_fails(self): skill = _temp_skill_copy(self) (skill / "templates" / "roles" / "03-designer.md").unlink() result = _run(skill) self.assertNotEqual( result.returncode, 0, "源缺失时校验必须失败退出非零") self.assertIn("03-designer", result.stderr) def test_tampered_source_fails(self): skill = _temp_skill_copy(self) target = skill / "templates" / "roles" / "06-qa.md" text = target.read_text(encoding="utf-8") target.write_text(text.replace("不把“没测”写成“通过”", "不把“没测”写成“通过X”"), encoding="utf-8") result = _run(skill) self.assertNotEqual( result.returncode, 0, "源被改动时校验必须失败退出非零") self.assertIn("06-qa", result.stderr) def test_extra_source_fails(self): skill = _temp_skill_copy(self) (skill / "templates" / "roles" / "99-extra.md").write_text( "# 角色模板:多余\n", encoding="utf-8") result = _run(skill) self.assertNotEqual( result.returncode, 0, "多余源文件时校验必须失败退出非零") self.assertIn("99-extra", result.stderr) def test_emit_is_headings_plus_sources(self): result = subprocess.run( [sys.executable, str(SCRIPT), "--emit"], capture_output=True, text=True) self.assertEqual(result.returncode, 0, result.stderr) expected_parts = [] for cid in EXPECTED_IDS: heading = ("## 附录角色:%s(来源 templates/roles/%s.md)" % (cid, cid)) source = (SKILL_DIR / "templates" / "roles" / (cid + ".md")).read_text(encoding="utf-8") expected_parts.append(heading + "\n\n" + source.rstrip("\n")) self.assertEqual(result.stdout, "\n\n".join(expected_parts) + "\n", "--emit 输出须恰为 12 章标题 + 12 份源正文") if __name__ == "__main__": unittest.main()