mirror of
https://github.com/rmyndharis/antigravity-skills.git
synced 2026-09-19 02:19:16 +08:00
5cd4ebdffa
skills-manager told the agent to run `python skills_cli.py <cmd>`, but installing the skill copies only SKILL.md — the script stays at the root of the checkout or npm package. Every documented command failed with "No such file or directory" on that path. The instructions now say where the script actually lives, point at the slash commands as the path that needs no resolution, and offer the ag-skills CLI when neither source is on disk. Examples call python3; `python` is absent on a stock macOS or Linux install. The Safety section claimed install "writes only under the global skills directory", which is false when AG_SKILLS_DIR is set — it now names the resolved destination and asks the agent to report the real path. agent-orchestration-multi-agent-optimize listed 'claude-4-sonnet' and 'claude-4-haiku' in a cost table. Neither has ever been a served model id; they are now claude-sonnet-4-6 and claude-haiku-4-5, with the per-1K input prices those models actually charge and the unit stated. plugin.json's version is maintained by hand and nothing compared it to package.json, so a release that bumped one and forgot the other would stay green forever. test_plugin_json_schema now asserts they match.
111 lines
5.0 KiB
Python
111 lines
5.0 KiB
Python
import unittest
|
|
import os
|
|
import json
|
|
import re
|
|
|
|
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
|
|
class TestSchemaValid(unittest.TestCase):
|
|
def test_plugin_json_schema(self):
|
|
plugin_path = os.path.join(REPO_ROOT, "plugin.json")
|
|
self.assertTrue(os.path.exists(plugin_path), "plugin.json does not exist at repo root")
|
|
|
|
with open(plugin_path, "r", encoding="utf-8") as f:
|
|
content = f.read()
|
|
|
|
data = json.loads(content)
|
|
self.assertIsInstance(data, dict, "plugin.json must contain a JSON object")
|
|
|
|
# Validate name
|
|
self.assertIn("name", data, "plugin.json missing 'name'")
|
|
self.assertIsInstance(data["name"], str)
|
|
self.assertTrue(re.match(r"^[a-z0-9]+(?:-[a-z0-9]+)*$", data["name"]), f"Invalid plugin name format: {data['name']}")
|
|
|
|
# Validate version
|
|
self.assertIn("version", data, "plugin.json missing 'version'")
|
|
self.assertIsInstance(data["version"], str)
|
|
self.assertTrue(re.match(r"^\d+\.\d+\.\d+$", data["version"]), f"Invalid semantic version: {data['version']}")
|
|
|
|
# plugin.json's version is maintained by hand and nothing else cross-checks it,
|
|
# so a release that bumps package.json and forgets this file stays green
|
|
# forever. Fail loudly at release time instead.
|
|
with open(os.path.join(REPO_ROOT, "package.json"), "r", encoding="utf-8") as f:
|
|
pkg_version = json.load(f)["version"]
|
|
self.assertEqual(
|
|
data["version"], pkg_version,
|
|
f"plugin.json version {data['version']} does not match package.json {pkg_version}",
|
|
)
|
|
|
|
# Validate description
|
|
self.assertIn("description", data, "plugin.json missing 'description'")
|
|
self.assertIsInstance(data["description"], str)
|
|
self.assertTrue(len(data["description"].strip()) > 0, "description cannot be empty")
|
|
self.assertLessEqual(len(data["description"]), 256, "description exceeds 256 chars")
|
|
|
|
# Validate entry
|
|
self.assertIn("entry", data, "plugin.json missing 'entry'")
|
|
self.assertEqual(data["entry"], "skills_cli.py")
|
|
|
|
# Validate skills list
|
|
self.assertIn("skills", data)
|
|
self.assertIsInstance(data["skills"], list)
|
|
self.assertIn("skills/antigravity-skills-manager/SKILL.md", data["skills"])
|
|
|
|
# Validate commands
|
|
self.assertIn("commands", data)
|
|
self.assertIsInstance(data["commands"], list)
|
|
self.assertTrue(len(data["commands"]) > 0)
|
|
cmds_by_name = {c["name"]: c for c in data["commands"] if isinstance(c, dict) and "name" in c}
|
|
for cmd_name in ["skills-manager", "skills"]:
|
|
self.assertIn(cmd_name, cmds_by_name, f"Command '{cmd_name}' missing from commands in plugin.json")
|
|
cmd = cmds_by_name[cmd_name]
|
|
self.assertIn("subcommands", cmd)
|
|
sub_names = [sc["name"] for sc in cmd["subcommands"] if isinstance(sc, dict) and "name" in sc]
|
|
self.assertIn("list", sub_names)
|
|
self.assertIn("search", sub_names)
|
|
self.assertIn("install", sub_names)
|
|
self.assertIn("installed", sub_names)
|
|
|
|
def test_skill_md_schema(self):
|
|
skill_md_path = os.path.join(REPO_ROOT, "skills", "antigravity-skills-manager", "SKILL.md")
|
|
self.assertTrue(os.path.exists(skill_md_path), f"SKILL.md missing at {skill_md_path}")
|
|
|
|
with open(skill_md_path, "r", encoding="utf-8") as f:
|
|
lines = f.readlines()
|
|
|
|
self.assertLessEqual(len(lines), 500, "SKILL.md exceeds 500 lines limit")
|
|
|
|
content = "".join(lines)
|
|
self.assertTrue(content.startswith("---"), "SKILL.md must start with YAML frontmatter delimiter '---'")
|
|
|
|
# Extract frontmatter
|
|
parts = content.split("---", 2)
|
|
self.assertGreaterEqual(len(parts), 3, "SKILL.md must have valid closing YAML frontmatter delimiter '---'")
|
|
frontmatter_raw = parts[1]
|
|
|
|
# Parse key-values from frontmatter
|
|
allowed_fields = {"name", "description", "license", "compatibility", "allowed-tools", "metadata"}
|
|
fm_keys = set()
|
|
for raw_line in frontmatter_raw.splitlines():
|
|
line = raw_line.strip()
|
|
if not line or line.startswith("#"):
|
|
continue
|
|
if ":" in line:
|
|
key = line.split(":", 1)[0].strip()
|
|
if not raw_line.startswith(" ") and not raw_line.startswith("\t"):
|
|
fm_keys.add(key)
|
|
|
|
for k in fm_keys:
|
|
self.assertIn(k, allowed_fields, f"Disallowed key '{k}' found in SKILL.md frontmatter")
|
|
|
|
self.assertIn("name", fm_keys, "Frontmatter missing 'name'")
|
|
self.assertIn("description", fm_keys, "Frontmatter missing 'description'")
|
|
|
|
# Section headers check
|
|
self.assertIn("## Use this skill when", content)
|
|
self.assertTrue("## Do not use" in content or "## Do not use this skill when" in content)
|
|
self.assertIn("## Instructions", content)
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|