import shutil import subprocess import sys import tempfile import unittest from pathlib import Path ROOT = Path(__file__).resolve().parents[1] SCRIPT = ROOT / "scripts" / "playbook.py" MODE_ROOTS = { "snapshot": Path("custom/playbook"), "subtree": Path("docs/standards/playbook"), } def run_playbook( script: Path, config: Path, project_root: Path ) -> subprocess.CompletedProcess[str]: return subprocess.run( [sys.executable, str(script), "-config", str(config)], cwd=project_root, capture_output=True, text=True, ) def copy_subtree_source(destination: Path) -> None: destination.mkdir(parents=True) shutil.copy2(ROOT / ".gitattributes", destination / ".gitattributes") for name in ("scripts", "templates"): shutil.copytree( ROOT / name, destination / name, ignore=shutil.ignore_patterns("__pycache__", "*.pyc"), ) (destination / "docs").mkdir() for name in ("common", "tsl", "markdown"): shutil.copytree(ROOT / "docs" / name, destination / "docs" / name) (destination / "rulesets").mkdir() shutil.copy2( ROOT / "rulesets" / "index.md", destination / "rulesets" / "index.md", ) for name in ("tsl", "markdown"): shutil.copytree( ROOT / "rulesets" / name, destination / "rulesets" / name, ) (destination / "skills").mkdir() for name in ("commit-message",): shutil.copytree( ROOT / "skills" / name, destination / "skills" / name, ) def write_config(project_root: Path, install_mode: str, playbook_root: Path) -> Path: config = project_root / "playbook.toml" config.write_text( f""" [playbook] project_root = "." playbook_root = "{playbook_root.as_posix()}" install_mode = "{install_mode}" [sync_rules] date = "2026-01-01" no_backup = true [sync_memory_bank] project_name = "Demo" no_backup = true [sync_prompts] no_backup = true [sync_standards] langs = ["tsl", "markdown"] gitattr_mode = "overwrite" no_backup = true [install_skills] agents_home = ".test-agents" mode = "list" skills = ["commit-message"] no_backup = true """.lstrip(), encoding="utf-8", newline="\n", ) return config def seed_custom_files(project_root: Path) -> None: custom_memory = project_root / "memory-bank" / "custom.md" custom_memory.parent.mkdir(parents=True) custom_memory.write_text("custom memory\n", encoding="utf-8", newline="\n") custom_prompt = project_root / "docs" / "prompts" / "custom.md" custom_prompt.parent.mkdir(parents=True) custom_prompt.write_text("custom prompt\n", encoding="utf-8", newline="\n") (project_root / "CLAUDE.md").write_text( "# Existing Claude\n\nKeep this.\n", encoding="utf-8", newline="\n", ) class PlaybookDeploymentTests(unittest.TestCase): def test_playbook_deployment_modes(self): for install_mode, playbook_root in MODE_ROOTS.items(): with self.subTest(install_mode=install_mode): with tempfile.TemporaryDirectory() as tmp_dir: project_root = Path(tmp_dir) / "project" project_root.mkdir() if install_mode == "subtree": source_root = project_root / playbook_root copy_subtree_source(source_root) script = source_root / "scripts" / "playbook.py" else: source_root = ROOT script = SCRIPT seed_custom_files(project_root) config = write_config(project_root, install_mode, playbook_root) for run_number in (1, 2): result = run_playbook(script, config, project_root) self.assertEqual( result.returncode, 0, msg=( f"{install_mode} run {run_number} failed\n" f"{result.stdout}{result.stderr}" ), ) expected_paths = ( "AGENTS.md", "AGENT_RULES.md", "AGENT_RULES.local.md", "CLAUDE.md", ".gitattributes", "memory-bank/project-brief.md", "memory-bank/active-context.md", "docs/prompts/system/agent-behavior.md", ".agents/index.md", ".agents/tsl/index.md", ".agents/markdown/index.md", ".test-agents/skills/commit-message/SKILL.md", ".test-agents/skills/commit-message/references/commit_policy.json", ".test-agents/skills/commit-message/scripts/validate_commit_message.py", ) missing = [ path for path in expected_paths if not (project_root / path).exists() ] self.assertEqual(missing, []) self.assertEqual( (project_root / "memory-bank/custom.md").read_text( encoding="utf-8" ), "custom memory\n", ) self.assertEqual( (project_root / "docs/prompts/custom.md").read_text( encoding="utf-8" ), "custom prompt\n", ) claude_text = (project_root / "CLAUDE.md").read_text( encoding="utf-8" ) self.assertIn("Keep this.", claude_text) self.assertEqual( claude_text.count(""), 1 ) docs_prefix = f"{playbook_root.as_posix()}/docs" agents_index = (project_root / ".agents/index.md").read_text( encoding="utf-8" ) self.assertIn(f"- {docs_prefix}", agents_index) source_skill = ROOT / "skills" / "commit-message" installed_commit_skill = ( project_root / ".test-agents/skills/commit-message" ) self.assertEqual( (installed_commit_skill / "SKILL.md").read_text(encoding="utf-8"), (source_skill / "SKILL.md").read_text(encoding="utf-8"), ) self.assertEqual( ( installed_commit_skill / "references/commit_policy.json" ).read_text(encoding="utf-8"), (source_skill / "references/commit_policy.json").read_text( encoding="utf-8" ), ) self.assertEqual( ( installed_commit_skill / "scripts/validate_commit_message.py" ).read_text(encoding="utf-8"), ( source_skill / "scripts/validate_commit_message.py" ).read_text(encoding="utf-8"), ) rules_text = (project_root / "AGENT_RULES.md").read_text( encoding="utf-8" ) self.assertIn( f"`{playbook_root.as_posix()}/` 是 Playbook 模板/供应商目录", rules_text, ) if install_mode == "snapshot": snapshot_root = project_root / playbook_root self.assertTrue((snapshot_root / "SOURCE.md").is_file()) self.assertTrue( (snapshot_root / "scripts/playbook.py").is_file() ) self.assertTrue((snapshot_root / "skills").is_dir()) else: self.assertFalse((source_root / "SOURCE.md").exists()) if __name__ == "__main__": unittest.main()