feat(bundles): add editorial bundle plugins
This commit is contained in:
@@ -5,10 +5,25 @@ const { findProjectRoot } = require("../../lib/project-root");
|
||||
|
||||
const projectRoot = findProjectRoot(__dirname);
|
||||
const marketplacePath = path.join(projectRoot, ".claude-plugin", "marketplace.json");
|
||||
const editorialBundlesPath = path.join(projectRoot, "data", "editorial-bundles.json");
|
||||
const marketplace = JSON.parse(fs.readFileSync(marketplacePath, "utf8"));
|
||||
const editorialBundles = JSON.parse(fs.readFileSync(editorialBundlesPath, "utf8")).bundles || [];
|
||||
|
||||
assert.ok(Array.isArray(marketplace.plugins), "marketplace.json must define a plugins array");
|
||||
assert.ok(marketplace.plugins.length > 0, "marketplace.json must contain at least one plugin");
|
||||
assert.strictEqual(
|
||||
marketplace.plugins[0]?.name,
|
||||
"antigravity-awesome-skills",
|
||||
"full library Claude plugin should remain the first marketplace entry",
|
||||
);
|
||||
|
||||
const expectedBundlePluginNames = editorialBundles.map((bundle) => `antigravity-bundle-${bundle.id}`);
|
||||
for (const pluginName of expectedBundlePluginNames) {
|
||||
assert.ok(
|
||||
marketplace.plugins.some((plugin) => plugin.name === pluginName),
|
||||
`marketplace.json must contain bundle plugin ${pluginName}`,
|
||||
);
|
||||
}
|
||||
|
||||
for (const plugin of marketplace.plugins) {
|
||||
assert.strictEqual(
|
||||
@@ -18,7 +33,7 @@ for (const plugin of marketplace.plugins) {
|
||||
);
|
||||
assert.ok(
|
||||
plugin.source.startsWith("./"),
|
||||
`plugin ${plugin.name || "<unnamed>"} source must be a relative path starting with ./`,
|
||||
`plugin ${plugin.name || "<unnamed>"} source must be a repo-relative path starting with ./`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,9 @@ const { findProjectRoot } = require("../../lib/project-root");
|
||||
|
||||
const projectRoot = findProjectRoot(__dirname);
|
||||
const marketplacePath = path.join(projectRoot, ".agents", "plugins", "marketplace.json");
|
||||
const editorialBundlesPath = path.join(projectRoot, "data", "editorial-bundles.json");
|
||||
const marketplace = JSON.parse(fs.readFileSync(marketplacePath, "utf8"));
|
||||
const editorialBundles = JSON.parse(fs.readFileSync(editorialBundlesPath, "utf8")).bundles || [];
|
||||
|
||||
assert.strictEqual(
|
||||
marketplace.name,
|
||||
@@ -19,6 +21,11 @@ assert.strictEqual(
|
||||
);
|
||||
assert.ok(Array.isArray(marketplace.plugins), "marketplace.json must define a plugins array");
|
||||
assert.ok(marketplace.plugins.length > 0, "marketplace.json must contain at least one plugin");
|
||||
assert.strictEqual(
|
||||
marketplace.plugins[0]?.name,
|
||||
"antigravity-awesome-skills",
|
||||
"full library Codex plugin should remain the first marketplace entry",
|
||||
);
|
||||
|
||||
const pluginEntry = marketplace.plugins.find((plugin) => plugin.name === "antigravity-awesome-skills");
|
||||
assert.ok(pluginEntry, "marketplace.json must include the antigravity-awesome-skills plugin entry");
|
||||
@@ -58,4 +65,23 @@ const pluginSkillsPath = path.join(pluginRoot, "skills");
|
||||
assert.ok(fs.existsSync(pluginSkillsPath), "Codex plugin skills path must exist");
|
||||
assert.ok(fs.statSync(pluginSkillsPath).isDirectory(), "Codex plugin skills path must be a directory");
|
||||
|
||||
for (const bundle of editorialBundles) {
|
||||
const bundlePluginName = `antigravity-bundle-${bundle.id}`;
|
||||
const bundleEntry = marketplace.plugins.find((plugin) => plugin.name === bundlePluginName);
|
||||
assert.ok(bundleEntry, `marketplace.json must include bundle plugin ${bundlePluginName}`);
|
||||
assert.deepStrictEqual(
|
||||
bundleEntry.source,
|
||||
{
|
||||
source: "local",
|
||||
path: `./plugins/${bundlePluginName}`,
|
||||
},
|
||||
`bundle plugin ${bundlePluginName} should resolve to the expected repo-local directory`,
|
||||
);
|
||||
assert.strictEqual(
|
||||
bundleEntry.category,
|
||||
bundle.group.replace(/^[^A-Za-z0-9]+/, "").trim(),
|
||||
`bundle plugin ${bundlePluginName} should derive its category from the bundle group`,
|
||||
);
|
||||
}
|
||||
|
||||
console.log("ok");
|
||||
|
||||
@@ -15,6 +15,7 @@ const LOCAL_TEST_COMMANDS = [
|
||||
[path.join(TOOL_TESTS, "build_catalog_bundles.test.js")],
|
||||
[path.join(TOOL_TESTS, "claude_plugin_marketplace.test.js")],
|
||||
[path.join(TOOL_TESTS, "codex_plugin_marketplace.test.js")],
|
||||
[path.join(TOOL_SCRIPTS, "run-python.js"), path.join(TOOL_TESTS, "test_editorial_bundles.py")],
|
||||
[path.join(TOOL_TESTS, "installer_antigravity_guidance.test.js")],
|
||||
[path.join(TOOL_TESTS, "jetski_gemini_loader.test.cjs")],
|
||||
[path.join(TOOL_TESTS, "npm_package_contents.test.js")],
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import importlib.util
|
||||
import json
|
||||
import pathlib
|
||||
import sys
|
||||
import tempfile
|
||||
@@ -27,23 +28,31 @@ class BundleActivationSecurityTests(unittest.TestCase):
|
||||
formatted = get_bundle_skills.format_skills_for_batch([
|
||||
"safe-skill",
|
||||
"nested.skill_2",
|
||||
"game-development/game-design",
|
||||
"unsafe&calc",
|
||||
"another|bad",
|
||||
])
|
||||
|
||||
self.assertEqual(formatted, "safe-skill\nnested.skill_2\n")
|
||||
self.assertEqual(formatted, "safe-skill\nnested.skill_2\ngame-development/game-design\n")
|
||||
|
||||
def test_get_bundle_skills_rejects_unsafe_bundle_entries(self):
|
||||
def test_get_bundle_skills_rejects_unsafe_bundle_entries_from_manifest(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
bundles_path = pathlib.Path(temp_dir) / "bundles.md"
|
||||
bundles_path = pathlib.Path(temp_dir) / "editorial-bundles.json"
|
||||
bundles_path.write_text(
|
||||
"\n".join(
|
||||
[
|
||||
"### Essentials",
|
||||
"- [`safe-skill`](../../skills/safe-skill/)",
|
||||
"- [`unsafe&calc`](../../skills/unsafe/)",
|
||||
"- [`safe_two`](../../skills/safe_two/)",
|
||||
]
|
||||
json.dumps(
|
||||
{
|
||||
"bundles": [
|
||||
{
|
||||
"id": "essentials",
|
||||
"name": "Essentials",
|
||||
"skills": [
|
||||
{"id": "safe-skill"},
|
||||
{"id": "unsafe&calc"},
|
||||
{"id": "safe_two"},
|
||||
],
|
||||
}
|
||||
]
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
@@ -55,6 +64,11 @@ class BundleActivationSecurityTests(unittest.TestCase):
|
||||
|
||||
self.assertEqual(skills, ["safe-skill", "safe_two"])
|
||||
|
||||
def test_nested_skill_ids_are_allowed_when_safe(self):
|
||||
self.assertTrue(get_bundle_skills.is_safe_skill_id("game-development/game-design"))
|
||||
self.assertFalse(get_bundle_skills.is_safe_skill_id("../escape"))
|
||||
self.assertFalse(get_bundle_skills.is_safe_skill_id("game-development/../escape"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
104
tools/scripts/tests/test_editorial_bundles.py
Normal file
104
tools/scripts/tests/test_editorial_bundles.py
Normal file
@@ -0,0 +1,104 @@
|
||||
import importlib.util
|
||||
import pathlib
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
|
||||
REPO_ROOT = pathlib.Path(__file__).resolve().parents[3]
|
||||
TOOLS_SCRIPTS = REPO_ROOT / "tools" / "scripts"
|
||||
|
||||
|
||||
def load_module(module_path: pathlib.Path, module_name: str):
|
||||
sys.path.insert(0, str(module_path.parent))
|
||||
spec = importlib.util.spec_from_file_location(module_name, module_path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
editorial_bundles = load_module(
|
||||
TOOLS_SCRIPTS / "sync_editorial_bundles.py",
|
||||
"sync_editorial_bundles",
|
||||
)
|
||||
get_bundle_skills = load_module(
|
||||
TOOLS_SCRIPTS / "get-bundle-skills.py",
|
||||
"get_bundle_skills_json",
|
||||
)
|
||||
|
||||
|
||||
class EditorialBundlesTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.manifest_bundles = editorial_bundles.load_editorial_bundles(REPO_ROOT)
|
||||
|
||||
def test_manifest_has_unique_ids_and_existing_skills(self):
|
||||
bundle_ids = [bundle["id"] for bundle in self.manifest_bundles]
|
||||
self.assertEqual(len(bundle_ids), len(set(bundle_ids)))
|
||||
|
||||
for bundle in self.manifest_bundles:
|
||||
self.assertEqual(bundle["id"], get_bundle_skills._normalize_bundle_query(bundle["name"]))
|
||||
self.assertTrue(bundle["skills"], f'bundle "{bundle["id"]}" should not be empty')
|
||||
for skill in bundle["skills"]:
|
||||
self.assertTrue((REPO_ROOT / "skills" / skill["id"]).exists())
|
||||
|
||||
def test_bundles_doc_matches_renderer(self):
|
||||
metadata = editorial_bundles.load_metadata(str(REPO_ROOT))
|
||||
expected = editorial_bundles.render_bundles_doc(REPO_ROOT, metadata, self.manifest_bundles)
|
||||
actual = (REPO_ROOT / "docs" / "users" / "bundles.md").read_text(encoding="utf-8")
|
||||
self.assertEqual(actual, expected)
|
||||
|
||||
def test_get_bundle_skills_reads_json_manifest_by_name_and_id(self):
|
||||
expected = ["concise-planning", "git-pushing", "kaizen", "lint-and-validate", "systematic-debugging"]
|
||||
self.assertEqual(get_bundle_skills.get_bundle_skills(["Essentials"]), expected)
|
||||
self.assertEqual(get_bundle_skills.get_bundle_skills(["essentials"]), expected)
|
||||
web_wizard_skills = get_bundle_skills.get_bundle_skills(["web-wizard"])
|
||||
self.assertIn("form-cro", web_wizard_skills)
|
||||
self.assertIn("react-best-practices", web_wizard_skills)
|
||||
self.assertIn(
|
||||
"game-development/game-design",
|
||||
get_bundle_skills.get_bundle_skills(["indie-game-dev"]),
|
||||
)
|
||||
|
||||
def test_generated_bundle_plugin_contains_expected_skills(self):
|
||||
essentials_plugin = REPO_ROOT / "plugins" / "antigravity-bundle-essentials" / "skills"
|
||||
expected_ids = {
|
||||
skill["id"]
|
||||
for skill in next(bundle for bundle in self.manifest_bundles if bundle["id"] == "essentials")["skills"]
|
||||
}
|
||||
actual_ids = {
|
||||
str(path.relative_to(essentials_plugin))
|
||||
for path in essentials_plugin.rglob("SKILL.md")
|
||||
}
|
||||
self.assertEqual(actual_ids, {f"{skill_id}/SKILL.md" for skill_id in expected_ids})
|
||||
|
||||
sample_skill_dir = essentials_plugin / "concise-planning"
|
||||
self.assertTrue((sample_skill_dir / "SKILL.md").is_file())
|
||||
|
||||
def test_generated_plugin_count_matches_manifest(self):
|
||||
generated_plugins = sorted(path.name for path in (REPO_ROOT / "plugins").iterdir() if path.is_dir() and path.name.startswith("antigravity-bundle-"))
|
||||
expected_plugins = sorted(f'antigravity-bundle-{bundle["id"]}' for bundle in self.manifest_bundles)
|
||||
self.assertEqual(generated_plugins, expected_plugins)
|
||||
|
||||
def test_sample_bundle_copy_matches_source_file_inventory(self):
|
||||
sample_bundle = next(bundle for bundle in self.manifest_bundles if bundle["id"] == "documents-presentations")
|
||||
plugin_skills_root = REPO_ROOT / "plugins" / "antigravity-bundle-documents-presentations" / "skills"
|
||||
|
||||
for skill in sample_bundle["skills"]:
|
||||
source_dir = REPO_ROOT / "skills" / skill["id"]
|
||||
copied_dir = plugin_skills_root / skill["id"]
|
||||
self.assertTrue(copied_dir.is_dir(), f'copied skill dir missing for {skill["id"]}')
|
||||
|
||||
source_files = sorted(
|
||||
str(path.relative_to(source_dir))
|
||||
for path in source_dir.rglob("*")
|
||||
if path.is_file()
|
||||
)
|
||||
copied_files = sorted(
|
||||
str(path.relative_to(copied_dir))
|
||||
for path in copied_dir.rglob("*")
|
||||
if path.is_file()
|
||||
)
|
||||
self.assertEqual(copied_files, source_files, f'copied bundle skill should match source inventory for {skill["id"]}')
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user