playbook/test/test_deployment_routes_e2e.py

204 lines
6.5 KiB
Python

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("<!-- playbook:claude:start -->", 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("<!-- playbook:claude:start -->"), 1)
self.assertFalse((project_root / "CLAUDE.md").exists())
if __name__ == "__main__":
unittest.main()