Files
skill-template/tests/test_scaffold_guard.py
chendelian 766d162245
All checks were successful
技能自动化发布 / release (push) Successful in 4s
fix: harden skill template contracts and scaffolding
Align scaffold slug replacement, Action manifest strict boundaries, metadata prune API, field permission validation, and task_logs readonly semantics with documented template contracts.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-11 11:55:46 +08:00

146 lines
5.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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()