import json import shutil 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_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()) def test_sync_standards_creates_agents(self): with tempfile.TemporaryDirectory() as tmp_dir: config_body = f""" [playbook] project_root = "{tmp_dir}" playbook_root = "{CUSTOM_DEPLOY_ROOT}" install_mode = "snapshot" [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}" playbook_root = "{CUSTOM_DEPLOY_ROOT}" install_mode = "snapshot" [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}" playbook_root = "{CUSTOM_DEPLOY_ROOT}" install_mode = "snapshot" [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_index_only_lists_configured_langs(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", "markdown"] """ 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) agents_index = (root / ".agents" / "index.md").read_text(encoding="utf-8") self.assertIn("`.agents/tsl/`:TSL 相关规则集", agents_index) self.assertIn("`.agents/markdown/`:Markdown 相关规则集", agents_index) self.assertNotIn("`.agents/cpp/`", agents_index) self.assertNotIn("`.agents/python/`", agents_index) self.assertNotIn("`.agents/typescript/`", 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}" playbook_root = "{CUSTOM_DEPLOY_ROOT}" install_mode = "snapshot" [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}" playbook_root = "{CUSTOM_DEPLOY_ROOT}" install_mode = "snapshot" [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) thirdparty_ref = subprocess.run( [ "git", f"--git-dir={mirror}", "rev-parse", "refs/remotes/origin/thirdparty/skill", ], capture_output=True, text=True, ) self.assertEqual(thirdparty_ref.returncode, 0, msg=thirdparty_ref.stderr) expose_thirdparty_branch = subprocess.run( [ "git", f"--git-dir={mirror}", "update-ref", "refs/heads/thirdparty/skill", thirdparty_ref.stdout.strip(), ], capture_output=True, text=True, ) self.assertEqual( expose_thirdparty_branch.returncode, 0, msg=expose_thirdparty_branch.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_data = json.loads(manifest_src.read_text(encoding="utf-8")) manifest_data["sources"] = [ entry for entry in manifest_data["sources"] if entry["id"] == "andrej-karpathy-skills" ] manifest_dst.write_text( json.dumps(manifest_data, indent=2) + "\n", encoding="utf-8", ) sync_src = ROOT / ".gitea" / "ci" / "sync_thirdparty_skills.sh" sync_dst = repo / ".gitea" / "ci" / "sync_thirdparty_skills.sh" shutil.copy2(sync_src, sync_dst) 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}" playbook_root = "{CUSTOM_DEPLOY_ROOT}" install_mode = "snapshot" [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}" playbook_root = "{CUSTOM_DEPLOY_ROOT}" install_mode = "snapshot" [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}" playbook_root = "{CUSTOM_DEPLOY_ROOT}" install_mode = "snapshot" [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_playbook_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}" playbook_root = "{CUSTOM_DEPLOY_ROOT}" install_mode = "snapshot" [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) install_config = write_config( root, "install.toml", f""" [playbook] project_root = "{tmp_dir}" playbook_root = "{CUSTOM_DEPLOY_ROOT}" install_mode = "snapshot" [sync_standards] langs = ["tsl"] """, ) install_result = run_cli("-config", str(install_config)) self.assertEqual( install_result.returncode, 0, msg=install_result.stdout + install_result.stderr, ) snapshot_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}" playbook_root = "{CUSTOM_DEPLOY_ROOT}" install_mode = "snapshot" [sync_standards] langs = ["tsl"] no_backup = true [install_skills] agents_home = "{agents_home}" mode = "list" skills = ["style-cleanup"] """, ) sync_result = run_script(snapshot_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" ) def test_sync_claude_md_creates_when_no_claude_md(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)) self.assertEqual(result.returncode, 0) claude_md = Path(tmp_dir) / "CLAUDE.md" self.assertTrue(claude_md.exists()) text = claude_md.read_text(encoding="utf-8") self.assertTrue(text.startswith("# CLAUDE.md\n\n")) self.assertIn("@AGENTS.md", text) self.assertIn("", text) def test_sync_claude_md_appends_block_to_existing_claude_md(self): with tempfile.TemporaryDirectory() as tmp_dir: claude_md = Path(tmp_dir) / "CLAUDE.md" claude_md.write_text("# My project\n\nSome existing content.\n", encoding="utf-8") 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)) self.assertEqual(result.returncode, 0) text = claude_md.read_text(encoding="utf-8") self.assertIn("@AGENTS.md", text) self.assertIn("@AGENT_RULES.md", text) self.assertIn("", text) self.assertIn("Some existing content.", text) def test_sync_claude_md_updates_existing_block(self): with tempfile.TemporaryDirectory() as tmp_dir: claude_md = Path(tmp_dir) / "CLAUDE.md" claude_md.write_text( "# My project\n\n" "\n" "@AGENTS.md\n" "\n", encoding="utf-8", ) 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)) self.assertEqual(result.returncode, 0) text = claude_md.read_text(encoding="utf-8") self.assertIn("@AGENTS.md", text) self.assertIn("@AGENT_RULES.md", text) self.assertEqual(text.count(""), 1) def test_sync_claude_md_adds_heading_to_generated_block(self): with tempfile.TemporaryDirectory() as tmp_dir: claude_md = Path(tmp_dir) / "CLAUDE.md" claude_md.write_text( "\n" "\n" "@AGENTS.md\n" "@AGENT_RULES.md\n" "\n" "\n", encoding="utf-8", ) 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)) self.assertEqual(result.returncode, 0) text = claude_md.read_text(encoding="utf-8") self.assertTrue(text.startswith("# CLAUDE.md\n\n")) self.assertIn("@AGENTS.md", text) self.assertIn("@AGENT_RULES.md", text) self.assertEqual(text.count(""), 1) def test_sync_claude_md_skips_when_already_references_agents(self): with tempfile.TemporaryDirectory() as tmp_dir: claude_md = Path(tmp_dir) / "CLAUDE.md" original = "# My project\n\n@AGENTS.md\n" claude_md.write_text(original, encoding="utf-8") 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)) self.assertEqual(result.returncode, 0) self.assertEqual(claude_md.read_text(encoding="utf-8"), original) def test_install_skills_creates_symlink_when_skill_link_configured(self): with tempfile.TemporaryDirectory() as tmp_dir: root = Path(tmp_dir) agents_home = root / "agents" link_home = root / "claude" config_body = f""" [playbook] project_root = "{tmp_dir}" playbook_root = "{CUSTOM_DEPLOY_ROOT}" install_mode = "snapshot" [install_skills] agents_home = "{agents_home}" skill_link = "{link_home}" mode = "list" skills = ["commit-message"] """ config_path = root / "playbook.toml" config_path.write_text(config_body, encoding="utf-8") result = run_cli("-config", str(config_path)) self.assertEqual(result.returncode, 0, msg=result.stdout + result.stderr) skills_dst = agents_home / "skills" link_path = link_home / "skills" self.assertTrue(skills_dst.is_dir()) self.assertTrue(link_path.is_dir(), "link_path should be accessible as dir") self.assertEqual(link_path.resolve(), skills_dst.resolve()) self.assertTrue((link_path / "commit-message" / "SKILL.md").is_file()) def test_install_skills_symlink_is_idempotent(self): with tempfile.TemporaryDirectory() as tmp_dir: root = Path(tmp_dir) agents_home = root / "agents" link_home = root / "claude" config_body = f""" [playbook] project_root = "{tmp_dir}" playbook_root = "{CUSTOM_DEPLOY_ROOT}" install_mode = "snapshot" [install_skills] agents_home = "{agents_home}" skill_link = "{link_home}" mode = "list" skills = ["commit-message"] no_backup = true """ config_path = root / "playbook.toml" config_path.write_text(config_body, encoding="utf-8") run_cli("-config", str(config_path)) result = run_cli("-config", str(config_path)) self.assertEqual(result.returncode, 0, msg=result.stdout + result.stderr) self.assertTrue((link_home / "skills").is_dir()) def test_install_skills_no_symlink_when_skill_link_absent(self): with tempfile.TemporaryDirectory() as tmp_dir: root = Path(tmp_dir) agents_home = root / "agents" config_body = f""" [playbook] project_root = "{tmp_dir}" playbook_root = "{CUSTOM_DEPLOY_ROOT}" install_mode = "snapshot" [install_skills] agents_home = "{agents_home}" mode = "list" skills = ["commit-message"] """ config_path = root / "playbook.toml" config_path.write_text(config_body, encoding="utf-8") result = run_cli("-config", str(config_path)) self.assertEqual(result.returncode, 0, msg=result.stdout + result.stderr) self.assertFalse(any( p.is_symlink() for p in (agents_home / "skills").iterdir() if p.is_symlink() ) if (agents_home / "skills").exists() else False) if __name__ == "__main__": unittest.main()