feat(playbook): add install_mode deployment config

BREAKING CHANGE: [vendor] and playbook.deploy_root are no longer supported. Use [playbook].install_mode and playbook.playbook_root instead.
This commit is contained in:
csh
2026-06-01 10:49:16 +08:00
parent 2c3a559a30
commit f2ab57b39f
15 changed files with 376 additions and 184 deletions
+2 -2
View File
@@ -16,7 +16,7 @@ tests/
├── test_no_backup_flags.py # no_backup 行为测试
├── test_playbook_typing_imports.py # playbook.py typing 导入兼容性测试
├── test_sync_directory_actions.py # sync_memory_bank/sync_prompts 行为测试
├── test_vendor_snapshot_templates.py # vendor 快照模板完整性测试
├── test_vendor_snapshot_templates.py # snapshot 快照模板完整性测试
├── test_main_loop_cli.py # main_loop CLI 测试
├── test_thirdparty_skills_pipeline.py # thirdparty skills 流水线配置与同步产物测试
├── test_sync_templates_placeholders.py # 占位符替换测试(sync_rules/sync_standards
@@ -70,7 +70,7 @@ sh tests/integration/check_doc_links.sh
- CLI 参数解析与帮助信息
- TOML 配置解析与动作顺序
- vendor/sync_rules/sync_memory_bank/sync_prompts/sync_standards 等基础动作落地
- snapshot install/sync_rules/sync_memory_bank/sync_prompts/sync_standards 等基础动作落地
### 2. 模板验证测试 (templates/)
+126 -42
View File
@@ -125,7 +125,8 @@ class PlaybookCliTests(unittest.TestCase):
config_body = f"""
[playbook]
project_root = "."
deploy_root = "{CUSTOM_DEPLOY_ROOT}"
playbook_root = "{CUSTOM_DEPLOY_ROOT}"
install_mode = "snapshot"
[format_md]
@@ -142,7 +143,7 @@ langs = ["tsl"]
self.assertIn("sync_standards", output)
self.assertIn("format_md", output)
def test_format_md_only_does_not_require_deploy_root(self):
def test_format_md_only_does_not_require_playbook_root(self):
with tempfile.TemporaryDirectory() as tmp_dir:
config_body = f"""
[playbook]
@@ -157,15 +158,55 @@ project_root = "{tmp_dir}"
self.assertEqual(result.returncode, 0)
def test_vendor_creates_snapshot(self):
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"
[vendor]
[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)
@@ -176,15 +217,16 @@ langs = ["tsl"]
self.assertEqual(result.returncode, 0)
self.assertTrue(snapshot.is_file())
def test_vendor_docs_index_uses_new_tsl_entrypoints(self):
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}"
deploy_root = "{CUSTOM_DEPLOY_ROOT}"
playbook_root = "{CUSTOM_DEPLOY_ROOT}"
install_mode = "snapshot"
[vendor]
[sync_standards]
langs = ["tsl"]
"""
config_path = write_config(root, "playbook.toml", config_body)
@@ -201,14 +243,15 @@ langs = ["tsl"]
self.assertIn("`tsl/reference/index.md`", text)
self.assertNotIn("`tsl/syntax_book/index.md`", text)
def test_external_clone_requires_explicit_deploy_root(self):
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"
[vendor]
[sync_standards]
langs = ["tsl"]
"""
config_path = write_config(root, "playbook.toml", config_body)
@@ -216,14 +259,34 @@ langs = ["tsl"]
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_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}"
deploy_root = "{CUSTOM_DEPLOY_ROOT}"
playbook_root = "{CUSTOM_DEPLOY_ROOT}"
install_mode = "snapshot"
[sync_memory_bank]
project_name = "Demo"
@@ -242,7 +305,8 @@ project_name = "Demo"
config_body = f"""
[playbook]
project_root = "{tmp_dir}"
deploy_root = "{CUSTOM_DEPLOY_ROOT}"
playbook_root = "{CUSTOM_DEPLOY_ROOT}"
install_mode = "snapshot"
[sync_standards]
langs = ["tsl"]
@@ -265,7 +329,8 @@ langs = ["tsl"]
f"""
[playbook]
project_root = "{tmp_dir}"
deploy_root = "{CUSTOM_DEPLOY_ROOT}"
playbook_root = "{CUSTOM_DEPLOY_ROOT}"
install_mode = "snapshot"
[sync_standards]
langs = ["tsl"]
@@ -282,7 +347,8 @@ no_backup = true
f"""
[playbook]
project_root = "{tmp_dir}"
deploy_root = "{CUSTOM_DEPLOY_ROOT}"
playbook_root = "{CUSTOM_DEPLOY_ROOT}"
install_mode = "snapshot"
[sync_standards]
langs = ["tsl", "cpp"]
@@ -304,7 +370,8 @@ no_backup = true
config_body = f"""
[playbook]
project_root = "{tmp_dir}"
deploy_root = "{CUSTOM_DEPLOY_ROOT}"
playbook_root = "{CUSTOM_DEPLOY_ROOT}"
install_mode = "snapshot"
[sync_standards]
langs = ["tsl", "markdown"]
@@ -326,7 +393,8 @@ langs = ["tsl", "markdown"]
config_body = f"""
[playbook]
project_root = "{tmp_dir}"
deploy_root = "{CUSTOM_DEPLOY_ROOT}"
playbook_root = "{CUSTOM_DEPLOY_ROOT}"
install_mode = "snapshot"
[sync_standards]
langs = ["tsl"]
@@ -352,7 +420,8 @@ langs = ["tsl"]
config_body = f"""
[playbook]
project_root = "{tmp_dir}"
deploy_root = "{CUSTOM_DEPLOY_ROOT}"
playbook_root = "{CUSTOM_DEPLOY_ROOT}"
install_mode = "snapshot"
[install_skills]
agents_home = "{target}"
@@ -453,7 +522,8 @@ skills = ["brainstorming"]
config_body = f"""
[playbook]
project_root = "{tmp_root}"
deploy_root = "{CUSTOM_DEPLOY_ROOT}"
playbook_root = "{CUSTOM_DEPLOY_ROOT}"
install_mode = "snapshot"
[install_skills]
agents_home = "{target}"
@@ -475,7 +545,8 @@ skills = ["karpathy-guidelines"]
config_body = f"""
[playbook]
project_root = "{tmp_dir}"
deploy_root = "{CUSTOM_DEPLOY_ROOT}"
playbook_root = "{CUSTOM_DEPLOY_ROOT}"
install_mode = "snapshot"
[install_skills]
agents_home = "{target}"
@@ -496,7 +567,8 @@ skills = ["tsl-guide"]
config_body = f"""
[playbook]
project_root = "{tmp_dir}"
deploy_root = "{CUSTOM_DEPLOY_ROOT}"
playbook_root = "{CUSTOM_DEPLOY_ROOT}"
install_mode = "snapshot"
[install_skills]
codex_home = "{target}"
@@ -511,17 +583,15 @@ skills = ["brainstorming"]
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):
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}"
deploy_root = "{CUSTOM_DEPLOY_ROOT}"
[vendor]
langs = ["tsl"]
playbook_root = "{CUSTOM_DEPLOY_ROOT}"
install_mode = "snapshot"
[sync_standards]
langs = ["tsl"]
@@ -545,23 +615,28 @@ skills = ["style-cleanup"]
def test_deployed_snapshot_rewrites_links_from_snapshot_location(self):
with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir)
vendor_config = write_config(
install_config = write_config(
root,
"vendor.toml",
"install.toml",
f"""
[playbook]
project_root = "{tmp_dir}"
deploy_root = "{CUSTOM_DEPLOY_ROOT}"
playbook_root = "{CUSTOM_DEPLOY_ROOT}"
install_mode = "snapshot"
[vendor]
[sync_standards]
langs = ["tsl"]
""",
)
vendor_result = run_cli("-config", str(vendor_config))
self.assertEqual(vendor_result.returncode, 0, msg=vendor_result.stdout + vendor_result.stderr)
install_result = run_cli("-config", str(install_config))
self.assertEqual(
install_result.returncode,
0,
msg=install_result.stdout + install_result.stderr,
)
vendored_script = root / CUSTOM_DEPLOY_ROOT / "scripts" / "playbook.py"
snapshot_script = root / CUSTOM_DEPLOY_ROOT / "scripts" / "playbook.py"
agents_home = root / "local-agents"
sync_config = write_config(
root,
@@ -569,7 +644,8 @@ langs = ["tsl"]
f"""
[playbook]
project_root = "{tmp_dir}"
deploy_root = "{CUSTOM_DEPLOY_ROOT}"
playbook_root = "{CUSTOM_DEPLOY_ROOT}"
install_mode = "snapshot"
[sync_standards]
langs = ["tsl"]
@@ -582,7 +658,7 @@ skills = ["style-cleanup"]
""",
)
sync_result = run_script(vendored_script, "-config", str(sync_config))
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"
@@ -593,7 +669,8 @@ skills = ["style-cleanup"]
config_body = f"""
[playbook]
project_root = "{tmp_dir}"
deploy_root = "{CUSTOM_DEPLOY_ROOT}"
playbook_root = "{CUSTOM_DEPLOY_ROOT}"
install_mode = "snapshot"
[sync_memory_bank]
project_name = "Demo"
@@ -619,7 +696,8 @@ project_name = "Demo"
config_body = f"""
[playbook]
project_root = "{tmp_dir}"
deploy_root = "{CUSTOM_DEPLOY_ROOT}"
playbook_root = "{CUSTOM_DEPLOY_ROOT}"
install_mode = "snapshot"
[sync_memory_bank]
project_name = "Demo"
@@ -650,7 +728,8 @@ project_name = "Demo"
config_body = f"""
[playbook]
project_root = "{tmp_dir}"
deploy_root = "{CUSTOM_DEPLOY_ROOT}"
playbook_root = "{CUSTOM_DEPLOY_ROOT}"
install_mode = "snapshot"
[sync_memory_bank]
project_name = "Demo"
@@ -682,7 +761,8 @@ project_name = "Demo"
config_body = f"""
[playbook]
project_root = "{tmp_dir}"
deploy_root = "{CUSTOM_DEPLOY_ROOT}"
playbook_root = "{CUSTOM_DEPLOY_ROOT}"
install_mode = "snapshot"
[sync_memory_bank]
project_name = "Demo"
@@ -708,7 +788,8 @@ project_name = "Demo"
config_body = f"""
[playbook]
project_root = "{tmp_dir}"
deploy_root = "{CUSTOM_DEPLOY_ROOT}"
playbook_root = "{CUSTOM_DEPLOY_ROOT}"
install_mode = "snapshot"
[sync_memory_bank]
project_name = "Demo"
@@ -730,7 +811,8 @@ project_name = "Demo"
config_body = f"""
[playbook]
project_root = "{tmp_dir}"
deploy_root = "{CUSTOM_DEPLOY_ROOT}"
playbook_root = "{CUSTOM_DEPLOY_ROOT}"
install_mode = "snapshot"
[install_skills]
agents_home = "{agents_home}"
@@ -759,7 +841,8 @@ skills = ["commit-message"]
config_body = f"""
[playbook]
project_root = "{tmp_dir}"
deploy_root = "{CUSTOM_DEPLOY_ROOT}"
playbook_root = "{CUSTOM_DEPLOY_ROOT}"
install_mode = "snapshot"
[install_skills]
agents_home = "{agents_home}"
@@ -784,7 +867,8 @@ no_backup = true
config_body = f"""
[playbook]
project_root = "{tmp_dir}"
deploy_root = "{CUSTOM_DEPLOY_ROOT}"
playbook_root = "{CUSTOM_DEPLOY_ROOT}"
install_mode = "snapshot"
[install_skills]
agents_home = "{agents_home}"
+5 -5
View File
@@ -85,6 +85,8 @@ class DeploymentRoutesE2ETests(unittest.TestCase):
"""
[playbook]
project_root = "."
playbook_root = "docs/standards/playbook"
install_mode = "subtree"
[sync_rules]
no_backup = true
@@ -119,7 +121,7 @@ no_backup = true
self.assertIn("@AGENT_RULES.md", text)
self.assertIn("<!-- playbook:claude:start -->", text)
def test_external_clone_deployment_vendors_snapshot_and_updates_claude(self):
def test_external_clone_deployment_installs_snapshot_and_updates_claude(self):
with tempfile.TemporaryDirectory() as tmp_dir:
tmp_root = Path(tmp_dir)
external_clone = tmp_root / "playbook"
@@ -137,10 +139,8 @@ no_backup = true
f"""
[playbook]
project_root = "."
deploy_root = "{CUSTOM_DEPLOY_ROOT.as_posix()}"
[vendor]
langs = ["tsl", "markdown"]
playbook_root = "{CUSTOM_DEPLOY_ROOT.as_posix()}"
install_mode = "snapshot"
[sync_rules]
no_backup = true
+2 -1
View File
@@ -34,7 +34,8 @@ class GitattributesModeTests(unittest.TestCase):
config_body = f"""
[playbook]
project_root = \"{root}\"
deploy_root = "{DEFAULT_DEPLOY_ROOT}"
playbook_root = "{DEFAULT_DEPLOY_ROOT}"
install_mode = "snapshot"
[sync_standards]
langs = [\"tsl\"]
+6 -3
View File
@@ -27,7 +27,8 @@ class NoBackupFlagsTests(unittest.TestCase):
config_body = f"""
[playbook]
project_root = "{tmp_dir}"
deploy_root = "{DEFAULT_DEPLOY_ROOT}"
playbook_root = "{DEFAULT_DEPLOY_ROOT}"
install_mode = "snapshot"
[sync_rules]
force = true
@@ -56,7 +57,8 @@ no_backup = true
config_body = f"""
[playbook]
project_root = "{tmp_dir}"
deploy_root = "{DEFAULT_DEPLOY_ROOT}"
playbook_root = "{DEFAULT_DEPLOY_ROOT}"
install_mode = "snapshot"
[sync_standards]
langs = ["tsl"]
@@ -85,7 +87,8 @@ no_backup = true
config_body = f"""
[playbook]
project_root = "{tmp_dir}"
deploy_root = "{DEFAULT_DEPLOY_ROOT}"
playbook_root = "{DEFAULT_DEPLOY_ROOT}"
install_mode = "snapshot"
[install_skills]
agents_home = "{root / 'agents'}"
+6 -3
View File
@@ -29,7 +29,8 @@ class SyncDirectoryActionsTests(unittest.TestCase):
config_body = f"""
[playbook]
project_root = "{tmp_dir}"
deploy_root = "{DEFAULT_DEPLOY_ROOT}"
playbook_root = "{DEFAULT_DEPLOY_ROOT}"
install_mode = "snapshot"
[sync_memory_bank]
project_name = "Demo"
@@ -54,7 +55,8 @@ project_name = "Demo"
config_body = f"""
[playbook]
project_root = "{tmp_dir}"
deploy_root = "{DEFAULT_DEPLOY_ROOT}"
playbook_root = "{DEFAULT_DEPLOY_ROOT}"
install_mode = "snapshot"
[sync_prompts]
"""
@@ -82,7 +84,8 @@ deploy_root = "{DEFAULT_DEPLOY_ROOT}"
config_body = f"""
[playbook]
project_root = "{tmp_dir}"
deploy_root = "{DEFAULT_DEPLOY_ROOT}"
playbook_root = "{DEFAULT_DEPLOY_ROOT}"
install_mode = "snapshot"
[sync_memory_bank]
project_name = "Demo"
+28 -24
View File
@@ -222,7 +222,8 @@ class SyncTemplatesPlaceholdersTests(unittest.TestCase):
config_body = f"""
[playbook]
project_root = \"{tmp_dir}\"
deploy_root = "{DEFAULT_DEPLOY_ROOT}"
playbook_root = "{DEFAULT_DEPLOY_ROOT}"
install_mode = "snapshot"
[sync_rules]
@@ -267,31 +268,16 @@ langs = [\"cpp\", \"tsl\"]
self.assertNotIn("{{PLAYBOOK_SCRIPTS}}", rules_text)
self.assertFalse(rules_text.endswith("\n\n"))
def test_sync_standards_rewrites_typescript_docs_prefix_for_vendored_playbook(self):
def test_sync_standards_rewrites_typescript_docs_prefix_for_snapshot_playbook(self):
with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir)
vendor_config = root / "vendor.toml"
vendor_config.write_text(
install_config = root / "install.toml"
install_config.write_text(
f"""
[playbook]
project_root = "{tmp_dir}"
deploy_root = "{DEFAULT_DEPLOY_ROOT}"
[vendor]
langs = ["typescript"]
""",
encoding="utf-8",
)
vendor_result = run_cli("-config", str(vendor_config))
self.assertEqual(vendor_result.returncode, 0, msg=vendor_result.stderr)
sync_config = root / "sync.toml"
sync_config.write_text(
f"""
[playbook]
project_root = "{tmp_dir}"
deploy_root = "{DEFAULT_DEPLOY_ROOT}"
playbook_root = "{DEFAULT_DEPLOY_ROOT}"
install_mode = "snapshot"
[sync_standards]
langs = ["typescript"]
@@ -299,11 +285,28 @@ langs = ["typescript"]
encoding="utf-8",
)
vendored_script = (
install_result = run_cli("-config", str(install_config))
self.assertEqual(install_result.returncode, 0, msg=install_result.stderr)
sync_config = root / "sync.toml"
sync_config.write_text(
f"""
[playbook]
project_root = "{tmp_dir}"
playbook_root = "{DEFAULT_DEPLOY_ROOT}"
install_mode = "snapshot"
[sync_standards]
langs = ["typescript"]
""",
encoding="utf-8",
)
snapshot_script = (
root / "docs" / "standards" / "playbook" / "scripts" / "playbook.py"
)
sync_result = run_script(
vendored_script, "-config", str(sync_config), cwd=root
snapshot_script, "-config", str(sync_config), cwd=root
)
self.assertEqual(sync_result.returncode, 0, msg=sync_result.stderr)
@@ -317,7 +320,8 @@ langs = ["typescript"]
config_body = f"""
[playbook]
project_root = "{tmp_dir}"
deploy_root = "{DEFAULT_DEPLOY_ROOT}"
playbook_root = "{DEFAULT_DEPLOY_ROOT}"
install_mode = "snapshot"
[sync_rules]
+7 -6
View File
@@ -66,19 +66,20 @@ class TslEntrypointsConsistencyTests(unittest.TestCase):
self.assertIn("方式一:git subtree", text)
self.assertIn("方式二:外部 clone 后执行部署", text)
self.assertIn("`project_root`:目标项目根目录", text)
self.assertIn("`deploy_root`:相对于 `project_root` 的项目内目标目录", text)
self.assertIn("`playbook_root`:相对于 `project_root` 的项目内 Playbook 根目录", text)
self.assertIn("不是外部 clone 出来的 Playbook 仓库路径", text)
self.assertIn("外部 clone 场景下必须显式填写 `deploy_root`", text)
self.assertIn("外部 clone 场景下必须显式填写 `playbook_root`", text)
self.assertNotIn("方式二:手动复制快照", text)
self.assertNotIn("方式三:CLI 裁剪复制", text)
self.assertNotIn("如果省略 `deploy_root`,默认仍部署到 `docs/standards/playbook`", text)
self.assertNotIn("如果省略 `playbook_root`,默认仍部署到 `docs/standards/playbook`", text)
def test_playbook_example_defines_deploy_root_as_target_path(self):
def test_playbook_example_defines_playbook_root_as_target_path(self):
text = PLAYBOOK_EXAMPLE.read_text(encoding="utf-8")
self.assertIn('deploy_root = "docs/standards/playbook"', text)
self.assertIn('playbook_root = "docs/standards/playbook"', text)
self.assertIn('install_mode = "subtree"', text)
self.assertIn("相对于 project_root", text)
self.assertIn("不是外部 clone 的 playbook 路径", text)
self.assertIn("从外部 clone 执行时必填", text)
self.assertIn("snapshot 表示从外部 clone 安装快照", text)
self.assertNotIn("target_dir", text)
def test_deployment_docs_do_not_reference_legacy_terms(self):
+5 -4
View File
@@ -17,15 +17,16 @@ def run_cli(*args):
)
class VendorSnapshotTemplatesTests(unittest.TestCase):
def test_vendor_includes_core_templates(self):
class SnapshotTemplatesTests(unittest.TestCase):
def test_snapshot_install_includes_core_templates(self):
with tempfile.TemporaryDirectory() as tmp_dir:
config_body = f"""
[playbook]
project_root = "{tmp_dir}"
deploy_root = "{DEFAULT_DEPLOY_ROOT}"
playbook_root = "{DEFAULT_DEPLOY_ROOT}"
install_mode = "snapshot"
[vendor]
[sync_standards]
langs = ["tsl"]
"""
config_path = Path(tmp_dir) / "playbook.toml"