# -*- coding: utf-8 -*- """skill-template 脚手架与 Git 防串库守护。""" from __future__ import annotations import json import os import tempfile import unittest from _support import get_skill_root TEMPLATE_MARKER = ".openclaw-skill-template" TEMPLATE_SLUG_KEBAB = "your-skill-slug" TEMPLATE_SLUG_UNDERSCORE = "your_skill_slug" SCAFFOLD_TEST_SLUG = "query-demo-records" SCAFFOLD_TEST_SLUG_UNDERSCORE = "query_demo_records" def _read_skill_slug() -> str: constants_path = os.path.join(get_skill_root(), "scripts", "util", "constants.py") with open(constants_path, encoding="utf-8") as f: for line in f: if line.strip().startswith("SKILL_SLUG"): _, _, value = line.partition("=") return value.strip().strip('"').strip("'") return "" class TestScaffoldGuard(unittest.TestCase): def test_template_repo_has_marker(self) -> None: slug = _read_skill_slug() if slug != TEMPLATE_SLUG_KEBAB: self.skipTest(f"当前 SKILL_SLUG={slug!r},非模板占位,跳过 template marker 断言") marker = os.path.join(get_skill_root(), TEMPLATE_MARKER) self.assertTrue( os.path.isfile(marker), msg=f"skill-template 源仓库根目录必须存在 {TEMPLATE_MARKER}", ) def test_business_skill_must_not_keep_template_marker(self) -> None: slug = _read_skill_slug() if slug == TEMPLATE_SLUG_KEBAB: self.skipTest("当前仍为模板占位 slug,由 test_template_repo_has_marker 守护") marker = os.path.join(get_skill_root(), TEMPLATE_MARKER) self.assertFalse( os.path.isfile(marker), msg=( "业务技能不得保留 .openclaw-skill-template;" "请使用 tools/scaffold_skill 或删除标记文件并检查是否误复制了模板 .git" ), ) def test_development_md_documents_git_cleanup(self) -> None: path = os.path.join(get_skill_root(), "development", "DEVELOPMENT.md") with open(path, encoding="utf-8") as f: text = f.read() self.assertIn(".git", text) self.assertTrue( "scaffold_skill" in text or "scaffold" in text.lower(), msg="DEVELOPMENT.md must document scaffold_skill", ) self.assertTrue( "remote" in text.lower() or "git remote" in text.lower(), msg="DEVELOPMENT.md must mention git remote self-check", ) def test_scaffold_replaces_slug_and_copies_actions_manifest(self) -> None: slug = _read_skill_slug() if slug != TEMPLATE_SLUG_KEBAB: self.skipTest("仅在 skill-template 源仓库运行 scaffold 集成测试") import subprocess import sys repo_root = get_skill_root() scaffold_py = os.path.join(repo_root, "tools", "scaffold_skill.py") with tempfile.TemporaryDirectory() as tmp: dest = os.path.join(tmp, SCAFFOLD_TEST_SLUG) proc = subprocess.run( [ sys.executable, scaffold_py, "--slug", SCAFFOLD_TEST_SLUG, "--destination", dest, ], cwd=repo_root, capture_output=True, text=True, ) self.assertEqual(proc.returncode, 0, msg=proc.stderr or proc.stdout) actions_path = os.path.join(dest, "assets", "actions.json") self.assertTrue(os.path.isfile(actions_path), msg="scaffold must copy assets/actions.json") with open(actions_path, encoding="utf-8") as f: manifest = json.load(f) self.assertEqual(manifest.get("skill"), SCAFFOLD_TEST_SLUG) manifest_raw = json.dumps(manifest) self.assertNotIn(TEMPLATE_SLUG_KEBAB, manifest_raw) self.assertNotIn(TEMPLATE_SLUG_UNDERSCORE, manifest_raw) actions_md = os.path.join(dest, "references", "ACTIONS.md") self.assertTrue(os.path.isfile(actions_md), msg="scaffold must copy references/ACTIONS.md") schema_path = os.path.join(dest, "assets", "schemas", "skill-actions.schema.json") self.assertTrue(os.path.isfile(schema_path)) marker = os.path.join(dest, TEMPLATE_MARKER) self.assertFalse(os.path.isfile(marker)) self.assertFalse(os.path.isdir(os.path.join(dest, ".git"))) constants_path = os.path.join(dest, "scripts", "util", "constants.py") with open(constants_path, encoding="utf-8") as f: constants_text = f.read() self.assertNotIn(TEMPLATE_SLUG_KEBAB, constants_text) self.assertNotIn(TEMPLATE_SLUG_UNDERSCORE, constants_text) self.assertIn(f'SKILL_SLUG = "{SCAFFOLD_TEST_SLUG}"', constants_text) self.assertIn( f'LOG_LOGGER_NAME = "openclaw.skill.{SCAFFOLD_TEST_SLUG_UNDERSCORE}"', constants_text, ) skill_md_path = os.path.join(dest, "SKILL.md") with open(skill_md_path, encoding="utf-8") as f: skill_md = f.read() self.assertIn(f"slug: {SCAFFOLD_TEST_SLUG}", skill_md) self.assertNotIn(TEMPLATE_SLUG_KEBAB, skill_md) self.assertNotIn(TEMPLATE_SLUG_UNDERSCORE, skill_md) def test_scaffold_ps1_matches_python_placeholder_rules(self) -> None: slug = _read_skill_slug() if slug != TEMPLATE_SLUG_KEBAB: self.skipTest("仅在 skill-template 源仓库运行 scaffold PS1 文案测试") ps1_path = os.path.join(get_skill_root(), "tools", "scaffold_skill.ps1") with open(ps1_path, encoding="utf-8") as f: text = f.read() self.assertIn("your-skill-slug", text) self.assertIn("your_skill_slug", text) self.assertIn('.Replace("-", "_")', text) if __name__ == "__main__": unittest.main()