import shutil import subprocess import sys import tempfile import unittest from pathlib import Path ROOT = Path(__file__).resolve().parents[1] DEFAULT_DEPLOY_ROOT = Path("docs/standards/playbook") CUSTOM_DEPLOY_ROOT = Path("custom/playbook") REPO_COPY_IGNORE = shutil.ignore_patterns( ".git", ".venv", "node_modules", "__pycache__", ".pytest_cache", ".mypy_cache", ".ruff_cache", ) def run_script( script: Path, *args: str, cwd: Path | None = None ) -> subprocess.CompletedProcess[str]: return subprocess.run( [sys.executable, str(script), *args], capture_output=True, text=True, cwd=cwd, ) def write_config(root: Path, name: str, body: str) -> Path: config_path = root / name config_path.write_text(body.strip() + "\n", encoding="utf-8") return config_path def copy_repo(target: Path) -> Path: shutil.copytree(ROOT, target, ignore=REPO_COPY_IGNORE) return target class DeploymentRoutesE2ETests(unittest.TestCase): def assert_core_project_files(self, project_root: Path) -> None: expected = [ "AGENTS.md", "AGENT_RULES.md", "AGENT_RULES.local.md", "memory-bank/progress.md", "docs/prompts/system/agent-behavior.md", ".agents/index.md", ".agents/tsl/index.md", ".agents/markdown/index.md", ".gitattributes", ] for rel in expected: with self.subTest(path=rel): self.assertTrue((project_root / rel).exists(), f"missing: {rel}") self.assertFalse((project_root / "docs" / "workflows").exists()) def assert_docs_prefix(self, project_root: Path, docs_prefix: str) -> None: agents_index = (project_root / ".agents" / "index.md").read_text(encoding="utf-8") self.assertIn("`.agents/tsl/index.md`", agents_index) self.assertIn("`.agents/markdown/index.md`", agents_index) self.assertIn(f"- {docs_prefix}", agents_index) tsl_index = (project_root / ".agents" / "tsl" / "index.md").read_text( encoding="utf-8" ) self.assertIn(f"`{docs_prefix}/tsl/index.md`", tsl_index) self.assertNotIn("`docs/tsl/index.md`", tsl_index) def test_subtree_style_deployment_syncs_project_files(self): with tempfile.TemporaryDirectory() as tmp_dir: tmp_root = Path(tmp_dir) project_root = tmp_root / "project" playbook_root = project_root / DEFAULT_DEPLOY_ROOT project_root.mkdir() copy_repo(playbook_root) config_path = write_config( project_root, "playbook.toml", """ [playbook] project_root = "." playbook_root = "docs/standards/playbook" install_mode = "subtree" [sync_rules] no_backup = true [sync_memory_bank] project_name = "Demo" [sync_prompts] no_backup = true [sync_standards] langs = ["tsl", "markdown"] no_backup = true """, ) result = run_script( playbook_root / "scripts" / "playbook.py", "-config", str(config_path), cwd=project_root, ) self.assertEqual(result.returncode, 0, msg=result.stdout + result.stderr) self.assert_core_project_files(project_root) self.assert_docs_prefix(project_root, "docs/standards/playbook/docs") claude_md = project_root / "CLAUDE.md" self.assertTrue(claude_md.is_file()) text = claude_md.read_text(encoding="utf-8") self.assertIn("@AGENTS.md", text) self.assertIn("@AGENT_RULES.md", text) self.assertIn("", text) def test_external_clone_deployment_installs_snapshot_and_updates_claude(self): with tempfile.TemporaryDirectory() as tmp_dir: tmp_root = Path(tmp_dir) external_clone = tmp_root / "playbook" project_root = tmp_root / "project" copy_repo(external_clone) project_root.mkdir() claude_md = project_root / ".claude" / "CLAUDE.md" claude_md.parent.mkdir() claude_md.write_text("# Existing Claude\n\nKeep this.\n", encoding="utf-8") config_path = write_config( project_root, "playbook.toml", f""" [playbook] project_root = "." playbook_root = "{CUSTOM_DEPLOY_ROOT.as_posix()}" install_mode = "snapshot" [sync_rules] no_backup = true [sync_memory_bank] project_name = "Demo" [sync_prompts] no_backup = true [sync_standards] langs = ["tsl", "markdown"] no_backup = true """, ) result = run_script( external_clone / "scripts" / "playbook.py", "-config", str(config_path), cwd=project_root, ) self.assertEqual(result.returncode, 0, msg=result.stdout + result.stderr) snapshot_root = project_root / CUSTOM_DEPLOY_ROOT self.assertTrue((snapshot_root / "SOURCE.md").is_file()) self.assertTrue((snapshot_root / "scripts" / "playbook.py").is_file()) self.assertTrue( (snapshot_root / "templates" / "AGENTS.template.md").is_file() ) self.assertTrue( (snapshot_root / "templates" / "AGENT_RULES.template.md").is_file() ) self.assertTrue((snapshot_root / "templates" / "README.md").is_file()) self.assertTrue((snapshot_root / "templates" / "memory-bank").is_dir()) self.assertTrue((snapshot_root / "templates" / "prompts").is_dir()) self.assertTrue((snapshot_root / "skills").is_dir()) self.assertFalse((snapshot_root / "codex").exists()) self.assert_core_project_files(project_root) self.assert_docs_prefix(project_root, "custom/playbook/docs") rules_text = (project_root / "AGENT_RULES.md").read_text(encoding="utf-8") self.assertIn( "`custom/playbook/` 是 Playbook 模板/供应商目录", rules_text, ) self.assertNotIn("{{PLAYBOOK_ROOT}}", rules_text) text = claude_md.read_text(encoding="utf-8") self.assertIn("Keep this.", text) self.assertIn("@../AGENTS.md", text) self.assertIn("@../AGENT_RULES.md", text) self.assertEqual(text.count(""), 1) self.assertFalse((project_root / "CLAUDE.md").exists()) if __name__ == "__main__": unittest.main()