import subprocess import sys import tempfile import unittest from pathlib import Path ROOT = Path(__file__).resolve().parents[2] SCRIPT = ROOT / "scripts" / "playbook.py" CUSTOM_DEPLOY_ROOT = "custom/playbook" def run_script(script, *args): return subprocess.run( [sys.executable, str(script), *args], capture_output=True, text=True, ) def run_cli(*args): return run_script(SCRIPT, *args) def write_config(root: Path, name: str, body: str) -> Path: config_path = root / name config_path.write_text(body, encoding="utf-8") return config_path class PlaybookCliTests(unittest.TestCase): def test_help_shows_usage(self): result = run_cli("-h") self.assertEqual(result.returncode, 0) self.assertIn("Usage:", result.stdout + result.stderr) def test_record_spec_updates_progress_workflow_state(self): with tempfile.TemporaryDirectory() as tmp_dir: root = Path(tmp_dir) progress = root / "memory-bank" / "progress.md" progress.parent.mkdir(parents=True) progress.write_text("# 当前进展\n", encoding="utf-8") result = run_cli( "-record-spec", "docs/superpowers/specs/2026-05-18-demo-design.md", "-progress", str(progress), ) self.assertEqual(result.returncode, 0, msg=result.stdout + result.stderr) text = progress.read_text(encoding="utf-8") self.assertIn("phase: planning", text) self.assertIn("spec: docs/superpowers/specs/2026-05-18-demo-design.md", text) def test_record_plan_updates_progress_workflow_state(self): with tempfile.TemporaryDirectory() as tmp_dir: root = Path(tmp_dir) progress = root / "memory-bank" / "progress.md" progress.parent.mkdir(parents=True) progress.write_text( "\n".join( [ "# 当前进展", "", "## Workflow State", "", "", "phase: planning", "spec: docs/superpowers/specs/2026-05-18-demo-design.md", "", ] ) + "\n", encoding="utf-8", ) result = run_cli( "-record-plan", "docs/superpowers/plans/2026-05-18-demo.md", "-progress", str(progress), ) self.assertEqual(result.returncode, 0, msg=result.stdout + result.stderr) text = progress.read_text(encoding="utf-8") self.assertIn("phase: planning", text) self.assertIn("spec: docs/superpowers/specs/2026-05-18-demo-design.md", text) self.assertIn("plan: docs/superpowers/plans/2026-05-18-demo.md", text) self.assertIn("executor: executing-plans", text) self.assertIn( "constraints: karpathy-guidelines,.agents,AGENT_RULES", text, ) def test_missing_config_is_error(self): result = run_cli() self.assertNotEqual(result.returncode, 0) self.assertIn("-config", result.stdout + result.stderr) def test_action_order(self): config_body = f""" [playbook] project_root = "." playbook_root = "{CUSTOM_DEPLOY_ROOT}" install_mode = "snapshot" [format_md] [sync_standards] langs = ["tsl"] """ with tempfile.TemporaryDirectory() as tmp_dir: config_path = Path(tmp_dir) / "playbook.toml" config_path.write_text(config_body, encoding="utf-8") result = run_cli("-config", str(config_path)) self.assertEqual(result.returncode, 0) output = result.stdout + result.stderr self.assertIn("sync_standards", output) self.assertIn("format_md", output) def test_format_md_only_does_not_require_playbook_root(self): with tempfile.TemporaryDirectory() as tmp_dir: config_body = f""" [playbook] project_root = "{tmp_dir}" [format_md] """ config_path = Path(tmp_dir) / "playbook.toml" config_path.write_text(config_body, encoding="utf-8") result = run_cli("-config", str(config_path)) self.assertEqual(result.returncode, 0) def test_vendor_section_is_rejected(self): with tempfile.TemporaryDirectory() as tmp_dir: root = Path(tmp_dir) config_body = f""" [playbook] project_root = "{tmp_dir}" playbook_root = "{CUSTOM_DEPLOY_ROOT}" install_mode = "snapshot" [vendor] langs = ["tsl"] """ config_path = write_config(root, "playbook.toml", config_body) result = run_cli("-config", str(config_path)) self.assertNotEqual(result.returncode, 0) self.assertIn("[vendor]", result.stdout + result.stderr) def test_deploy_root_is_rejected(self): with tempfile.TemporaryDirectory() as tmp_dir: root = Path(tmp_dir) config_body = f""" [playbook] project_root = "{tmp_dir}" deploy_root = "{CUSTOM_DEPLOY_ROOT}" install_mode = "snapshot" [sync_standards] langs = ["tsl"] """ config_path = write_config(root, "playbook.toml", config_body) result = run_cli("-config", str(config_path)) self.assertNotEqual(result.returncode, 0) self.assertIn("deploy_root", result.stdout + result.stderr) self.assertIn("playbook_root", result.stdout + result.stderr) def test_snapshot_install_creates_snapshot(self): with tempfile.TemporaryDirectory() as tmp_dir: root = Path(tmp_dir) config_body = f""" [playbook] project_root = "{tmp_dir}" playbook_root = "{CUSTOM_DEPLOY_ROOT}" install_mode = "snapshot" [sync_standards] langs = ["tsl"] """ config_path = write_config(root, "playbook.toml", config_body) result = run_cli("-config", str(config_path)) snapshot = root / CUSTOM_DEPLOY_ROOT / "SOURCE.md" self.assertEqual(result.returncode, 0) self.assertTrue(snapshot.is_file()) def test_snapshot_docs_index_uses_new_tsl_entrypoints(self): with tempfile.TemporaryDirectory() as tmp_dir: root = Path(tmp_dir) config_body = f""" [playbook] project_root = "{tmp_dir}" playbook_root = "{CUSTOM_DEPLOY_ROOT}" install_mode = "snapshot" [sync_standards] langs = ["tsl"] """ config_path = write_config(root, "playbook.toml", config_body) result = run_cli("-config", str(config_path)) docs_index = root / CUSTOM_DEPLOY_ROOT / "docs/index.md" self.assertEqual(result.returncode, 0) text = docs_index.read_text(encoding="utf-8") self.assertIn("`tsl/index.md`", text) self.assertIn("`tsl/syntax/index.md`", text) self.assertIn("`tsl/finance/index.md`", text) self.assertIn("`tsl/modules/index.md`", text) self.assertIn("`tsl/reference/index.md`", text) self.assertNotIn("`tsl/syntax_book/index.md`", text) def test_external_clone_requires_explicit_playbook_root(self): with tempfile.TemporaryDirectory() as tmp_dir: root = Path(tmp_dir) config_body = f""" [playbook] project_root = "{tmp_dir}" install_mode = "snapshot" [sync_standards] langs = ["tsl"] """ config_path = write_config(root, "playbook.toml", config_body) result = run_cli("-config", str(config_path)) self.assertNotEqual(result.returncode, 0) self.assertIn("playbook_root", result.stdout + result.stderr) def test_subtree_mode_requires_project_local_script(self): with tempfile.TemporaryDirectory() as tmp_dir: root = Path(tmp_dir) config_body = f""" [playbook] project_root = "{tmp_dir}" playbook_root = "{CUSTOM_DEPLOY_ROOT}" install_mode = "subtree" [sync_standards] langs = ["tsl"] """ config_path = write_config(root, "playbook.toml", config_body) result = run_cli("-config", str(config_path)) self.assertNotEqual(result.returncode, 0) self.assertIn("project-local Playbook script", result.stdout + result.stderr) def test_sync_memory_bank_creates_memory_bank(self): with tempfile.TemporaryDirectory() as tmp_dir: config_body = f""" [playbook] project_root = "{tmp_dir}" playbook_root = "{CUSTOM_DEPLOY_ROOT}" install_mode = "snapshot" [sync_memory_bank] project_name = "Demo" """ config_path = Path(tmp_dir) / "playbook.toml" config_path.write_text(config_body, encoding="utf-8") result = run_cli("-config", str(config_path)) memory_bank = Path(tmp_dir) / "memory-bank/project-brief.md" self.assertEqual(result.returncode, 0) self.assertTrue(memory_bank.is_file()) if __name__ == "__main__": unittest.main()