Files
rmyndharis 5cd4ebdffa fix: correct skills-manager instructions, invalid model ids, and version drift
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.
2026-08-03 01:19:43 +07:00

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()