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 def bash_path(path: Path) -> str: resolved = path.resolve() if sys.platform != "win32": return resolved.as_posix() drive = resolved.drive.rstrip(":").lower() rest = resolved.as_posix()[2:] return f"/mnt/{drive}{rest}" class PlaybookCliTests(unittest.TestCase): def assert_style_cleanup_tsl_docs_prefix( self, root: Path, agents_home: Path, docs_prefix: str ) -> None: agents_index = (root / ".agents" / "tsl" / "index.md").read_text(encoding="utf-8") self.assertIn(f"`{docs_prefix}/tsl/index.md`", agents_index) self.assertNotIn("`docs/tsl/index.md`", agents_index) skill_file = (agents_home / "skills" / "style-cleanup" / "SKILL.md").read_text( encoding="utf-8" ) self.assertIn(f"`{docs_prefix}/tsl/code_style.md`", skill_file) self.assertNotIn("`docs/tsl/code_style.md`", skill_file) def test_help_shows_usage(self): result = run_cli("-h") self.assertEqual(result.returncode, 0) self.assertIn("Usage:", result.stdout + result.stderr) 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 = "." deploy_root = "{CUSTOM_DEPLOY_ROOT}" [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_deploy_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_creates_snapshot(self): with tempfile.TemporaryDirectory() as tmp_dir: root = Path(tmp_dir) config_body = f""" [playbook] project_root = "{tmp_dir}" deploy_root = "{CUSTOM_DEPLOY_ROOT}" [vendor] 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_vendor_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}" deploy_root = "{CUSTOM_DEPLOY_ROOT}" [vendor] 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_deploy_root(self): with tempfile.TemporaryDirectory() as tmp_dir: root = Path(tmp_dir) config_body = f""" [playbook] project_root = "{tmp_dir}" [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("deploy_root", 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}" deploy_root = "{CUSTOM_DEPLOY_ROOT}" [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()) def test_sync_standards_creates_agents(self): with tempfile.TemporaryDirectory() as tmp_dir: config_body = f""" [playbook] project_root = "{tmp_dir}" deploy_root = "{CUSTOM_DEPLOY_ROOT}" [sync_standards] langs = ["tsl"] """ config_path = Path(tmp_dir) / "playbook.toml" config_path.write_text(config_body, encoding="utf-8") result = run_cli("-config", str(config_path)) agents_index = Path(tmp_dir) / ".agents/tsl/index.md" self.assertEqual(result.returncode, 0) self.assertTrue(agents_index.is_file()) def test_sync_standards_updates_agents_index_when_langs_expand(self): with tempfile.TemporaryDirectory() as tmp_dir: root = Path(tmp_dir) first_config = root / "playbook-first.toml" first_config.write_text( f""" [playbook] project_root = "{tmp_dir}" deploy_root = "{CUSTOM_DEPLOY_ROOT}" [sync_standards] langs = ["tsl"] no_backup = true """, encoding="utf-8", ) first_result = run_cli("-config", str(first_config)) self.assertEqual(first_result.returncode, 0) second_config = root / "playbook-second.toml" second_config.write_text( f""" [playbook] project_root = "{tmp_dir}" deploy_root = "{CUSTOM_DEPLOY_ROOT}" [sync_standards] langs = ["tsl", "cpp"] no_backup = true """, encoding="utf-8", ) second_result = run_cli("-config", str(second_config)) self.assertEqual(second_result.returncode, 0) agents_index = (root / ".agents" / "index.md").read_text(encoding="utf-8") self.assertIn("`.agents/tsl/index.md`", agents_index) self.assertIn("`.agents/cpp/index.md`", agents_index) def test_sync_standards_agents_block_has_blank_lines(self): with tempfile.TemporaryDirectory() as tmp_dir: config_body = f""" [playbook] project_root = "{tmp_dir}" deploy_root = "{CUSTOM_DEPLOY_ROOT}" [sync_standards] langs = ["tsl"] """ 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) agents_md = Path(tmp_dir) / "AGENTS.md" lines = agents_md.read_text(encoding="utf-8").splitlines() start_idx = lines.index("") end_idx = lines.index("") block = lines[start_idx : end_idx + 1] self.assertEqual(block[1], "") bullet_idx = next(i for i, line in enumerate(block) if line.startswith("- ")) self.assertEqual(block[bullet_idx - 1], "") def test_install_skills(self): with tempfile.TemporaryDirectory() as tmp_dir: target = Path(tmp_dir) / "agents" config_body = f""" [playbook] project_root = "{tmp_dir}" deploy_root = "{CUSTOM_DEPLOY_ROOT}" [install_skills] agents_home = "{target}" mode = "list" skills = ["brainstorming"] """ config_path = Path(tmp_dir) / "playbook.toml" config_path.write_text(config_body, encoding="utf-8") result = run_cli("-config", str(config_path)) skill_file = target / "skills/brainstorming/SKILL.md" self.assertEqual(result.returncode, 0) self.assertTrue(skill_file.is_file()) def test_install_generated_thirdparty_karpathy_skill_after_sync(self): with tempfile.TemporaryDirectory() as tmp_dir: tmp_root = Path(tmp_dir) mirror = tmp_root / "origin.git" repo = tmp_root / "repo" target = tmp_root / "agents" clone_mirror = subprocess.run( ["git", "clone", "--mirror", str(ROOT), str(mirror)], capture_output=True, text=True, ) self.assertEqual(clone_mirror.returncode, 0, msg=clone_mirror.stderr) clone_repo = subprocess.run( ["git", "clone", str(mirror), str(repo)], capture_output=True, text=True, ) self.assertEqual(clone_repo.returncode, 0, msg=clone_repo.stderr) set_remote = subprocess.run( ["git", "-C", str(repo), "remote", "set-url", "origin", bash_path(mirror)], capture_output=True, text=True, ) self.assertEqual(set_remote.returncode, 0, msg=set_remote.stderr) manifest_src = ROOT / ".gitea" / "ci" / "thirdparty_skills.json" manifest_dst = repo / ".gitea" / "ci" / "thirdparty_skills.json" manifest_dst.write_text(manifest_src.read_text(encoding="utf-8"), encoding="utf-8") sync_result = subprocess.run( ["bash", ".gitea/ci/sync_thirdparty_skills.sh"], cwd=repo, capture_output=True, text=True, ) self.assertEqual( sync_result.returncode, 0, msg=sync_result.stdout + sync_result.stderr ) config_body = f""" [playbook] project_root = "{tmp_root}" deploy_root = "{CUSTOM_DEPLOY_ROOT}" [install_skills] agents_home = "{target}" mode = "list" skills = ["karpathy-guidelines"] """ config_path = tmp_root / "playbook.toml" config_path.write_text(config_body, encoding="utf-8") result = run_script(repo / "scripts" / "playbook.py", "-config", str(config_path)) skill_file = target / "skills" / "karpathy-guidelines" / "SKILL.md" self.assertEqual(result.returncode, 0, msg=result.stdout + result.stderr) self.assertTrue(skill_file.is_file()) def test_install_skills_rejects_removed_tsl_guide(self): with tempfile.TemporaryDirectory() as tmp_dir: target = Path(tmp_dir) / "agents" config_body = f""" [playbook] project_root = "{tmp_dir}" deploy_root = "{CUSTOM_DEPLOY_ROOT}" [install_skills] agents_home = "{target}" mode = "list" skills = ["tsl-guide"] """ config_path = Path(tmp_dir) / "playbook.toml" config_path.write_text(config_body, encoding="utf-8") result = run_cli("-config", str(config_path)) self.assertNotEqual(result.returncode, 0) self.assertIn("skill not found: tsl-guide", result.stdout + result.stderr) def test_install_skills_rejects_codex_home(self): with tempfile.TemporaryDirectory() as tmp_dir: target = Path(tmp_dir) / "codex" config_body = f""" [playbook] project_root = "{tmp_dir}" deploy_root = "{CUSTOM_DEPLOY_ROOT}" [install_skills] codex_home = "{target}" mode = "list" skills = ["brainstorming"] """ config_path = Path(tmp_dir) / "playbook.toml" config_path.write_text(config_body, encoding="utf-8") result = run_cli("-config", str(config_path)) self.assertNotEqual(result.returncode, 0) self.assertIn("codex_home", result.stdout + result.stderr) def test_external_clone_flow_rewrites_links_with_configured_deploy_root(self): with tempfile.TemporaryDirectory() as tmp_dir: root = Path(tmp_dir) agents_home = root / "agents-home" config_body = f""" [playbook] project_root = "{tmp_dir}" deploy_root = "{CUSTOM_DEPLOY_ROOT}" [vendor] langs = ["tsl"] [sync_standards] langs = ["tsl"] no_backup = true [install_skills] agents_home = "{agents_home}" mode = "list" skills = ["style-cleanup"] """ config_path = write_config(root, "playbook.toml", config_body) result = run_cli("-config", str(config_path)) self.assertEqual(result.returncode, 0, msg=result.stdout + result.stderr) self.assertTrue((root / CUSTOM_DEPLOY_ROOT / "SOURCE.md").is_file()) self.assert_style_cleanup_tsl_docs_prefix( root, agents_home, f"{CUSTOM_DEPLOY_ROOT}/docs" ) def test_deployed_snapshot_rewrites_links_from_snapshot_location(self): with tempfile.TemporaryDirectory() as tmp_dir: root = Path(tmp_dir) vendor_config = write_config( root, "vendor.toml", f""" [playbook] project_root = "{tmp_dir}" deploy_root = "{CUSTOM_DEPLOY_ROOT}" [vendor] langs = ["tsl"] """, ) vendor_result = run_cli("-config", str(vendor_config)) self.assertEqual(vendor_result.returncode, 0, msg=vendor_result.stdout + vendor_result.stderr) vendored_script = root / CUSTOM_DEPLOY_ROOT / "scripts" / "playbook.py" agents_home = root / "local-agents" sync_config = write_config( root, "sync.toml", f""" [playbook] project_root = "{tmp_dir}" deploy_root = "{CUSTOM_DEPLOY_ROOT}" [sync_standards] langs = ["tsl"] no_backup = true [install_skills] agents_home = "{agents_home}" mode = "list" skills = ["style-cleanup"] """, ) sync_result = run_script(vendored_script, "-config", str(sync_config)) self.assertEqual(sync_result.returncode, 0, msg=sync_result.stdout + sync_result.stderr) self.assert_style_cleanup_tsl_docs_prefix( root, agents_home, f"{CUSTOM_DEPLOY_ROOT}/docs" ) if __name__ == "__main__": unittest.main()