from __future__ import annotations import subprocess import sys import tempfile import unittest from pathlib import Path REPO_ROOT = Path(__file__).resolve().parents[1] CATALOG = REPO_ROOT / "skills" / "pouch" / "scripts" / "catalog.py" SUGGEST = REPO_ROOT / "skills" / "pouch" / "scripts" / "suggest_skills.py" def _run(script: Path, *args: str) -> subprocess.CompletedProcess[str]: return subprocess.run( [sys.executable, str(script), *args], check=False, capture_output=True, text=True, ) class CatalogTests(unittest.TestCase): def test_repo_skills_have_lede_and_example(self) -> None: result = _run(CATALOG, "--skills-dir", str(REPO_ROOT / "skills")) self.assertEqual(result.returncode, 0, result.stderr) for name in ( "ack", "builder", "deployer", "host-sec-check", "releaser", "note-style", "pouch", "short-ship", "long-ship", "ship", "pr-ship", # 旧名兼容占位,仍须可列出(见 test_manage_release_is_deprecation_stub) "manage-release", "bugfix-ship", "requirement-flow", ): self.assertIn(f"| {name} |", result.stdout) self.assertIn("/ack 初始化", result.stdout) self.assertIn("用 builder 初始化这个项目", result.stdout) self.assertNotIn("触发词", result.stdout) def test_manage_release_is_deprecation_stub(self) -> None: """旧名只保留指向 releaser 的占位,不得再携带流程或参考资料。""" stub = REPO_ROOT / "skills" / "manage-release" skill = (stub / "SKILL.md").read_text(encoding="utf-8") self.assertIn("name: manage-release", skill) self.assertIn("已弃用", skill) self.assertIn("../releaser/SKILL.md", skill) self.assertTrue((REPO_ROOT / "skills" / "releaser" / "SKILL.md").is_file()) self.assertFalse((stub / "references").exists()) self.assertFalse((stub / "templates").exists()) def test_bugfix_ship_is_deprecation_stub(self) -> None: stub = REPO_ROOT / "skills" / "bugfix-ship" skill = (stub / "SKILL.md").read_text(encoding="utf-8") self.assertIn("name: bugfix-ship", skill) self.assertIn("已弃用", skill) self.assertIn("../ship/SKILL.md", skill) self.assertTrue((REPO_ROOT / "skills" / "ship" / "SKILL.md").is_file()) self.assertFalse((stub / "references").exists()) self.assertFalse((stub / "templates").exists()) self.assertFalse((stub / "agents").exists()) def test_requirement_flow_is_deprecation_stub(self) -> None: stub = REPO_ROOT / "skills" / "requirement-flow" skill = (stub / "SKILL.md").read_text(encoding="utf-8") self.assertIn("name: requirement-flow", skill) self.assertIn("已弃用", skill) self.assertIn("../ship/SKILL.md", skill) self.assertTrue((REPO_ROOT / "skills" / "ship" / "SKILL.md").is_file()) self.assertFalse((stub / "references").exists()) self.assertFalse((stub / "templates").exists()) self.assertFalse((stub / "agents").exists()) def test_short_ship_is_deprecation_stub(self) -> None: stub = REPO_ROOT / "skills" / "short-ship" skill = (stub / "SKILL.md").read_text(encoding="utf-8") self.assertIn("name: short-ship", skill) self.assertIn("已弃用", skill) self.assertIn("../ship/SKILL.md", skill) self.assertIn("short 模式", skill) self.assertTrue((REPO_ROOT / "skills" / "ship" / "SKILL.md").is_file()) self.assertFalse((stub / "references").exists()) self.assertFalse((stub / "templates").exists()) self.assertFalse((stub / "agents").exists()) def test_long_ship_is_deprecation_stub(self) -> None: stub = REPO_ROOT / "skills" / "long-ship" skill = (stub / "SKILL.md").read_text(encoding="utf-8") self.assertIn("name: long-ship", skill) self.assertIn("已弃用", skill) self.assertIn("../ship/SKILL.md", skill) self.assertIn("long 模式", skill) self.assertTrue((REPO_ROOT / "skills" / "ship" / "SKILL.md").is_file()) self.assertFalse((stub / "references").exists()) self.assertFalse((stub / "templates").exists()) self.assertFalse((stub / "agents").exists()) def test_missing_readme_fails(self) -> None: with tempfile.TemporaryDirectory() as tmp: skill = Path(tmp) / "demo" skill.mkdir() (skill / "SKILL.md").write_text( "---\nname: demo\ndescription: d\n---\n# demo\n", encoding="utf-8", ) result = _run(CATALOG, "--skills-dir", tmp) self.assertEqual(result.returncode, 1) self.assertIn("缺少 README.md", result.stderr) def test_extracts_first_sentence_and_skips_hash_comments(self) -> None: with tempfile.TemporaryDirectory() as tmp: skill = Path(tmp) / "demo" skill.mkdir() (skill / "SKILL.md").write_text( "---\nname: demo\ndescription: d\n---\n# demo\n", encoding="utf-8", ) (skill / "README.md").write_text( "# demo\n\n" "这是一句话说明。后面还有一句不该进短清单。\n\n" "## 使用示例\n\n" "```text\n" "# 注释\n" "用 demo 做这件事。\n" "```\n", encoding="utf-8", ) result = _run(CATALOG, "--skills-dir", tmp) self.assertEqual(result.returncode, 0, result.stderr) self.assertIn("这是一句话说明。", result.stdout) self.assertNotIn("不该进短清单", result.stdout) self.assertIn("用 demo 做这件事。", result.stdout) class SuggestSkillsTests(unittest.TestCase): def test_pouch_repo_is_not_an_app(self) -> None: result = _run(SUGGEST, str(REPO_ROOT)) self.assertEqual(result.returncode, 0, result.stderr) self.assertIn("this_is_pouch_repo: true", result.stdout) self.assertNotIn("suggested:", result.stdout) def test_dockerfile_suggests_builder(self) -> None: with tempfile.TemporaryDirectory() as tmp: root = Path(tmp) (root / "Dockerfile").write_text("FROM scratch\n", encoding="utf-8") result = _run(SUGGEST, str(root)) self.assertEqual(result.returncode, 0, result.stderr) self.assertIn("this_is_pouch_repo: false", result.stdout) self.assertIn("builder", result.stdout) self.assertIn("Dockerfile", result.stdout) self.assertIn("用 builder 初始化", result.stdout) self.assertIn("不要 `pouch init builder`", result.stdout) def test_existing_makefile_builder_is_already(self) -> None: with tempfile.TemporaryDirectory() as tmp: root = Path(tmp) (root / "makefile.builder").write_text("build:\n", encoding="utf-8") result = _run(SUGGEST, str(root)) self.assertEqual(result.returncode, 0, result.stderr) self.assertIn("already:", result.stdout) self.assertIn("builder", result.stdout) self.assertIn("init: (none)", result.stdout) def test_compose_and_ack_dir(self) -> None: with tempfile.TemporaryDirectory() as tmp: root = Path(tmp) (root / "compose.yaml").write_text("services: {}\n", encoding="utf-8") (root / ".pouch" / "ack").mkdir(parents=True) result = _run(SUGGEST, str(root)) self.assertEqual(result.returncode, 0, result.stderr) self.assertIn("deployer", result.stdout) self.assertIn("compose.yaml", result.stdout) self.assertIn("ack", result.stdout) self.assertIn(".pouch/ack", result.stdout) self.assertIn("用 deployer 初始化", result.stdout) self.assertNotIn("/ack 初始化", result.stdout) if __name__ == "__main__": unittest.main()