242 lines
8.4 KiB
Python
242 lines
8.4 KiB
Python
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("<!-- playbook:claude:start -->"), 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()
|