Files
playbook/test/test_playbook.py
T
csh 699b431cac feat(workflow): adopt Matt Pocock ticket workflow
Replace the Superpowers plan pipeline with grill-with-docs, specs, local tickets, and ticket-native execution.

BREAKING CHANGE: Remove the legacy Plan CLI, prompt templates, and Superpowers skills.
2026-08-10 16:55:46 +08:00

406 lines
14 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_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")
(project_root / "AGENTS.md").write_text(
"# Existing Agents\n\nKeep this agent note.\n\nSee `.agents/index.md`.\n",
encoding="utf-8",
newline="\n",
)
(project_root / "CLAUDE.md").write_text(
"# Existing Claude\n\nKeep this Claude note.\n\n@AGENTS.md\n",
encoding="utf-8",
newline="\n",
)
class PlaybookDeploymentTests(unittest.TestCase):
def test_help_lists_only_current_cli_options(self):
with tempfile.TemporaryDirectory() as tmp_dir:
result = subprocess.run(
[sys.executable, str(SCRIPT), "--help"],
cwd=Path(tmp_dir),
capture_output=True,
text=True,
)
self.assertEqual(result.returncode, 0, msg=result.stderr)
self.assertIn("-config PATH", result.stdout)
self.assertIn("-h, --help", result.stdout)
self.assertNotIn("-h, -help", result.stdout)
def test_install_all_excludes_legacy_superpowers_skills(self):
with tempfile.TemporaryDirectory() as tmp_dir:
project_root = Path(tmp_dir) / "project"
project_root.mkdir()
config = project_root / "playbook.toml"
config.write_text(
"""
[playbook]
project_root = "."
playbook_root = "custom/playbook"
install_mode = "snapshot"
[install_skills]
agents_home = ".test-agents"
mode = "all"
no_backup = true
""".lstrip(),
encoding="utf-8",
newline="\n",
)
result = run_playbook(SCRIPT, config, project_root)
self.assertEqual(
result.returncode,
0,
msg=f"all-skills install failed\n{result.stdout}{result.stderr}",
)
installed = {
path.name
for path in (project_root / ".test-agents" / "skills").iterdir()
if path.is_dir()
}
self.assertIn("grill-with-docs", installed)
self.assertIn("to-tickets", installed)
self.assertTrue(
{
"using-superpowers",
"brainstorming",
"writing-plans",
"executing-plans",
}.isdisjoint(installed)
)
def test_install_skills_list_requires_explicit_skills(self):
with tempfile.TemporaryDirectory() as tmp_dir:
project_root = Path(tmp_dir) / "project"
project_root.mkdir()
config = project_root / "playbook.toml"
config.write_text(
"""
[playbook]
project_root = "."
playbook_root = "custom/playbook"
install_mode = "snapshot"
[install_skills]
agents_home = ".test-agents"
mode = "list"
bundles = ["matt-pocock-workflow"]
no_backup = true
""".lstrip(),
encoding="utf-8",
newline="\n",
)
result = run_playbook(SCRIPT, config, project_root)
self.assertEqual(
result.returncode,
2,
msg=f"bundles unexpectedly supported\n{result.stdout}{result.stderr}",
)
self.assertIn("ERROR: skills is required", result.stderr)
def test_invalid_toml_is_reported_without_a_traceback(self):
with tempfile.TemporaryDirectory() as tmp_dir:
project_root = Path(tmp_dir) / "project"
project_root.mkdir()
config = project_root / "playbook.toml"
config.write_text(
r"""
[playbook]
project_root = "C:\workspace\project"
""".lstrip(),
encoding="utf-8",
newline="\n",
)
result = run_playbook(SCRIPT, config, project_root)
self.assertEqual(result.returncode, 2)
self.assertIn("ERROR: invalid TOML", result.stderr)
self.assertNotIn("Traceback", result.stderr)
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/system-patterns.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",
)
agents_text = (project_root / "AGENTS.md").read_text(
encoding="utf-8"
)
self.assertIn("Keep this agent note.", agents_text)
self.assertEqual(
agents_text.count("<!-- playbook:framework:start -->"), 1
)
claude_text = (project_root / "CLAUDE.md").read_text(
encoding="utf-8"
)
self.assertIn("Keep this Claude note.", claude_text)
self.assertIn("@AGENTS.md", 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 / "scripts/main_loop.py").is_file()
)
self.assertTrue(
(snapshot_root / "playbook.example.toml").is_file()
)
self.assertFalse(
(snapshot_root / "playbook.toml.example").exists()
)
self.assertFalse(
(snapshot_root / "scripts/ticket_loop.py").exists()
)
self.assertTrue((snapshot_root / "skills").is_dir())
else:
self.assertFalse((source_root / "SOURCE.md").exists())
def test_sync_without_standards_keeps_language_rules(self):
with tempfile.TemporaryDirectory() as tmp_dir:
project_root = Path(tmp_dir) / "project"
project_root.mkdir()
playbook_root = MODE_ROOTS["snapshot"]
full_config = write_config(project_root, "snapshot", playbook_root)
result = run_playbook(SCRIPT, full_config, project_root)
self.assertEqual(
result.returncode,
0,
msg=f"seed run failed\n{result.stdout}{result.stderr}",
)
agents_md = project_root / "AGENTS.md"
seeded = agents_md.read_text(encoding="utf-8")
self.assertIn("`.agents/tsl/index.md`", seeded)
self.assertIn("`.agents/markdown/index.md`", seeded)
partial_config = project_root / "playbook-partial.toml"
partial_config.write_text(
f"""
[playbook]
project_root = "."
playbook_root = "{playbook_root.as_posix()}"
install_mode = "snapshot"
[sync_memory_bank]
project_name = "Demo"
no_backup = true
""".lstrip(),
encoding="utf-8",
newline="\n",
)
result = run_playbook(SCRIPT, partial_config, project_root)
self.assertEqual(
result.returncode,
0,
msg=f"partial run failed\n{result.stdout}{result.stderr}",
)
after = agents_md.read_text(encoding="utf-8")
self.assertIn("`.agents/tsl/index.md`", after)
self.assertIn("`.agents/markdown/index.md`", after)
if __name__ == "__main__":
unittest.main()