234 lines
9.7 KiB
Python
234 lines
9.7 KiB
Python
"""Tests for init_skill.py: scaffolding each shape and checking skills for the frontmatter and bmod rules."""
|
|
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import tomllib
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
SCRIPT = Path(__file__).resolve().parents[1] / "init_skill.py"
|
|
|
|
GOOD_DESCRIPTION = "Counts things in a folder. Use when the user asks how big a skill is."
|
|
|
|
|
|
def run(*args):
|
|
res = subprocess.run([sys.executable, str(SCRIPT), *[str(a) for a in args]], capture_output=True, text=True)
|
|
data = json.loads(res.stdout) if res.stdout.strip().startswith("{") else None
|
|
return res.returncode, data, res.stderr
|
|
|
|
|
|
def rules(data):
|
|
return sorted(f["rule"] for f in data["findings"])
|
|
|
|
|
|
class CreateTest(unittest.TestCase):
|
|
def test_plain_skill_with_dirs_and_skill_table(self):
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
code, data, err = run(
|
|
"--name",
|
|
"bmad-demo",
|
|
"--dest",
|
|
tmp,
|
|
"--shape",
|
|
"plain-skill",
|
|
"--dirs",
|
|
"references,scripts",
|
|
"--bmod",
|
|
"bmod-example",
|
|
"--source",
|
|
"github:acme/example/skills",
|
|
"--description",
|
|
GOOD_DESCRIPTION,
|
|
)
|
|
self.assertEqual(code, 0, err)
|
|
skill = Path(tmp) / "bmad-demo"
|
|
self.assertEqual(data["created"], ["SKILL.md", "bmod.toml", "references/", "scripts/"])
|
|
self.assertEqual(data["shape"], "plain-skill")
|
|
text = (skill / "SKILL.md").read_text(encoding="utf-8")
|
|
self.assertTrue(text.startswith("---\nname: bmad-demo\ndescription: '"))
|
|
self.assertIn("# bmad-demo", text)
|
|
bmod = tomllib.loads((skill / "bmod.toml").read_text(encoding="utf-8"))
|
|
self.assertEqual(bmod, {"skill": {"bmod": "bmod-example", "source": "github:acme/example/skills"}})
|
|
self.assertTrue((skill / "references").is_dir())
|
|
# The new skill passes its own check.
|
|
code, data, _ = run("--check", skill)
|
|
self.assertEqual(code, 0, data)
|
|
self.assertEqual(data["bmod"], "ok")
|
|
|
|
def test_no_bmod_without_flag(self):
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
target = Path(tmp) / "bmad-demo"
|
|
code, data, err = run(
|
|
"--name", "bmad-demo", "--dest", tmp, "--shape", "plain-skill", "--description", GOOD_DESCRIPTION
|
|
)
|
|
self.assertEqual(code, 0, err)
|
|
self.assertEqual(data["created"], ["SKILL.md"])
|
|
self.assertFalse((target / "bmod.toml").exists())
|
|
code, data, _ = run("--check", target)
|
|
self.assertEqual(code, 0, data)
|
|
self.assertEqual(data["bmod"], "absent")
|
|
|
|
def test_placeholder_description_fails_check(self):
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
code, _, _ = run("--name", "bmad-demo", "--dest", tmp, "--shape", "script-utility")
|
|
self.assertEqual(code, 0)
|
|
code, data, _ = run("--check", Path(tmp) / "bmad-demo")
|
|
self.assertEqual(code, 1)
|
|
# The placeholder carries "Use when", so only the TODO marker fails the check.
|
|
self.assertEqual(rules(data), ["todo-left"])
|
|
|
|
def test_single_skill_module(self):
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
code, _, _ = run(
|
|
"--name",
|
|
"bmad-changelog",
|
|
"--dest",
|
|
tmp,
|
|
"--shape",
|
|
"single-skill-module",
|
|
"--source",
|
|
"github:acme/changelog",
|
|
"--description",
|
|
GOOD_DESCRIPTION,
|
|
)
|
|
self.assertEqual(code, 0)
|
|
bmod = tomllib.loads((Path(tmp) / "bmad-changelog" / "bmod.toml").read_text(encoding="utf-8"))
|
|
self.assertEqual(
|
|
bmod["bmod"], {"code": "changelog", "version": "0.1.0", "update_source": "github:acme/changelog"}
|
|
)
|
|
self.assertEqual(bmod["skill"], {})
|
|
code, data, _ = run("--check", Path(tmp) / "bmad-changelog")
|
|
self.assertEqual(code, 0, data)
|
|
|
|
def test_multi_skill_module_record(self):
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
code, _, _ = run(
|
|
"--name",
|
|
"bmod-acme",
|
|
"--dest",
|
|
tmp,
|
|
"--shape",
|
|
"multi-skill-module",
|
|
"--source",
|
|
"github:acme/x/skills",
|
|
)
|
|
self.assertEqual(code, 0)
|
|
skill = Path(tmp) / "bmod-acme"
|
|
bmod = tomllib.loads((skill / "bmod.toml").read_text(encoding="utf-8"))
|
|
self.assertEqual(
|
|
bmod,
|
|
{"bmod": {"code": "acme", "version": "0.1.0", "update_source": "github:acme/x/skills", "skills": []}},
|
|
)
|
|
self.assertIn(
|
|
"Required bmod metadata. Never invoke this skill.", (skill / "SKILL.md").read_text(encoding="utf-8")
|
|
)
|
|
code, data, _ = run("--check", skill)
|
|
self.assertEqual(code, 0, data)
|
|
code, _, _ = run("--name", "bmad-acme", "--dest", tmp, "--shape", "multi-skill-module")
|
|
self.assertEqual(code, 1)
|
|
|
|
def test_refuses_bad_name_existing_folder_and_unknown_dir(self):
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
code, data, _ = run("--name", "Bad_Name", "--dest", tmp, "--shape", "plain-skill")
|
|
self.assertEqual(code, 1)
|
|
self.assertFalse(data["ok"])
|
|
(Path(tmp) / "bmad-taken").mkdir()
|
|
code, _, _ = run("--name", "bmad-taken", "--dest", tmp, "--shape", "plain-skill")
|
|
self.assertEqual(code, 1)
|
|
code, _, _ = run("--name", "bmad-new", "--dest", tmp, "--shape", "plain-skill", "--dirs", "steps")
|
|
self.assertEqual(code, 1)
|
|
code, _, _ = run("--name", "bmad-new", "--dest", tmp)
|
|
self.assertEqual(code, 2)
|
|
|
|
|
|
class CheckTest(unittest.TestCase):
|
|
def write_skill(self, tmp, name, skill_md, bmod="[skill]\n", extra=None):
|
|
skill = Path(tmp) / name
|
|
skill.mkdir()
|
|
(skill / "SKILL.md").write_text(skill_md, encoding="utf-8")
|
|
if bmod is not None:
|
|
(skill / "bmod.toml").write_text(bmod, encoding="utf-8")
|
|
for rel, text in (extra or {}).items():
|
|
path = skill / rel
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text(text, encoding="utf-8")
|
|
return skill
|
|
|
|
def test_good_skill_with_multiline_description(self):
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
skill = self.write_skill(
|
|
tmp,
|
|
"bmad-good",
|
|
"---\nname: bmad-good\ndescription: >-\n Does a thing.\n Use when asked.\n---\n\nBody.\n",
|
|
extra={".memlog.md": "- (note) [TODO: hidden files are not scanned]\n"},
|
|
)
|
|
code, data, _ = run("--check", skill)
|
|
self.assertEqual(code, 0, data)
|
|
self.assertEqual(data, {"ok": True, "skill": "bmad-good", "bmod": "ok", "findings": []})
|
|
|
|
def test_every_rule(self):
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
skill = self.write_skill(
|
|
tmp,
|
|
"my-skill",
|
|
"---\nname: other-name\ndescription: '" + "x" * 1025 + "'\nversion: 1\n---\n\n \n",
|
|
bmod="[skill\nbroken",
|
|
extra={"references/a.md": "[TODO: write]\n"},
|
|
)
|
|
code, data, _ = run("--check", skill)
|
|
self.assertEqual(code, 1)
|
|
self.assertEqual(
|
|
rules(data),
|
|
[
|
|
"bmod-invalid",
|
|
"body-empty",
|
|
"description-length",
|
|
"description-trigger",
|
|
"frontmatter-keys",
|
|
"name-folder-mismatch",
|
|
"name-format",
|
|
"todo-left",
|
|
],
|
|
)
|
|
todo = next(f for f in data["findings"] if f["rule"] == "todo-left")
|
|
self.assertEqual((todo["path"], todo["line"]), ("references/a.md", 1))
|
|
|
|
def test_any_name_and_missing_pieces(self):
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
skill = self.write_skill(
|
|
tmp,
|
|
"acme-notes",
|
|
"---\nname: acme-notes\ndescription: 'Notes. Use when writing.'\n---\n\nBody.\n",
|
|
bmod=None,
|
|
)
|
|
code, data, _ = run("--check", skill)
|
|
self.assertEqual(rules(data), ["name-format"])
|
|
self.assertEqual(data["bmod"], "absent")
|
|
code, data, _ = run("--check", skill, "--any-name")
|
|
self.assertEqual(code, 0, data)
|
|
empty = Path(tmp) / "bmad-empty"
|
|
empty.mkdir()
|
|
code, data, _ = run("--check", empty)
|
|
self.assertEqual(rules(data), ["skill-md-missing"])
|
|
no_fm = self.write_skill(tmp, "bmad-nofm", "# Just a heading\n", bmod="[other]\n")
|
|
code, data, _ = run("--check", no_fm)
|
|
self.assertEqual(rules(data), ["bmod-tables", "frontmatter-missing"])
|
|
self.assertEqual(data["bmod"], "invalid")
|
|
|
|
def test_record_rules(self):
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
record = self.write_skill(
|
|
tmp,
|
|
"bmod-acme",
|
|
"---\nname: bmod-acme\ndescription: 'Acme tools. Use when needed.'\n---\n\nBody.\n",
|
|
bmod='[bmod]\ncode = "acme"\nversion = "1.0.0"\nupdate_source = "github:acme/x"\n',
|
|
)
|
|
code, data, _ = run("--check", record)
|
|
self.assertEqual(rules(data), ["description-trigger"])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|