mirror of
https://github.com/vintasoftware/django-ai-plugins.git
synced 2026-09-14 16:38:37 +08:00
376 lines
16 KiB
Python
376 lines
16 KiB
Python
import copy
|
|
import importlib.util
|
|
import json
|
|
import shutil
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
from unittest import mock
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
|
|
def load_script(name):
|
|
path = ROOT / "scripts" / f"{name}.py"
|
|
spec = importlib.util.spec_from_file_location(name, path)
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
generator = load_script("generate_adapters")
|
|
smoke = load_script("smoke_plugins")
|
|
|
|
|
|
class NativeDistributionTests(unittest.TestCase):
|
|
def load_catalog(self):
|
|
return json.loads((ROOT / "plugins" / "catalog.json").read_text())
|
|
|
|
def test_marketplaces_have_the_same_five_stable_ids(self):
|
|
expected = [plugin["id"] for plugin in self.load_catalog()["plugins"]]
|
|
claude = json.loads((ROOT / ".claude-plugin" / "marketplace.json").read_text())
|
|
codex = json.loads((ROOT / ".agents" / "plugins" / "marketplace.json").read_text())
|
|
cursor = json.loads((ROOT / ".cursor-plugin" / "marketplace.json").read_text())
|
|
|
|
self.assertEqual([plugin["name"] for plugin in claude["plugins"]], expected)
|
|
self.assertEqual([plugin["name"] for plugin in codex["plugins"]], expected)
|
|
self.assertEqual([plugin["name"] for plugin in cursor["plugins"]], expected)
|
|
|
|
def test_native_manifests_match_catalog_and_expose_one_host_surface(self):
|
|
for plugin in self.load_catalog()["plugins"]:
|
|
package = ROOT / plugin["package"]
|
|
for target in ("claude", "codex", "cursor"):
|
|
with self.subTest(plugin=plugin["id"], target=target):
|
|
manifest = json.loads(
|
|
(
|
|
package
|
|
/ f".{target}-plugin"
|
|
/ "plugin.json"
|
|
).read_text()
|
|
)
|
|
self.assertEqual(manifest["name"], plugin["id"])
|
|
self.assertEqual(manifest["version"], plugin["version"])
|
|
self.assertEqual(manifest["description"], plugin["description"])
|
|
capabilities = smoke.discover_capabilities(package, target)
|
|
self.assertEqual(len(capabilities), 1)
|
|
self.assertTrue(capabilities[0]["path"].is_file())
|
|
|
|
def test_isolated_smoke_never_uses_real_user_homes(self):
|
|
for target in ("claude", "codex", "cursor", "opencode", "agent-skills"):
|
|
with self.subTest(target=target):
|
|
with tempfile.TemporaryDirectory(dir="/tmp") as temporary:
|
|
home = Path(temporary)
|
|
results = smoke.smoke_repository(ROOT, target, home)
|
|
|
|
self.assertEqual(len(results), 5)
|
|
self.assertTrue(
|
|
all(result["status"] == "ok" for result in results)
|
|
)
|
|
if target == "opencode":
|
|
self.assertTrue((home / "checkout" / "skills").is_dir())
|
|
self.assertTrue(
|
|
all(
|
|
result["provenance"] == "opencode-plugin"
|
|
for result in results
|
|
)
|
|
)
|
|
elif target == "agent-skills":
|
|
self.assertTrue((home / "skills").is_dir())
|
|
else:
|
|
self.assertTrue(
|
|
all(
|
|
(home / "plugins" / result["plugin"]).is_dir()
|
|
for result in results
|
|
)
|
|
)
|
|
|
|
def test_install_update_and_uninstall_preserve_other_plugins(self):
|
|
plugins = {
|
|
plugin["id"]: plugin for plugin in self.load_catalog()["plugins"]
|
|
}
|
|
with tempfile.TemporaryDirectory() as temporary_directory:
|
|
home = Path(temporary_directory)
|
|
first = smoke.install_package(ROOT, home, plugins["django-expert"])
|
|
second = smoke.install_package(ROOT, home, plugins["cdrf-expert"])
|
|
marker = first / "local-marker"
|
|
marker.write_text("stale")
|
|
|
|
updated = smoke.install_package(ROOT, home, plugins["django-expert"])
|
|
|
|
self.assertFalse((updated / "local-marker").exists())
|
|
self.assertTrue(second.is_dir())
|
|
smoke.uninstall_package(home, "django-expert")
|
|
self.assertFalse(updated.exists())
|
|
self.assertTrue(second.is_dir())
|
|
self.assertTrue((ROOT / "plugins" / "django-expert").is_dir())
|
|
|
|
def test_public_smoke_reports_duplicate_provenance_and_remediation(self):
|
|
with tempfile.TemporaryDirectory(dir="/tmp") as temporary:
|
|
home = Path(temporary)
|
|
direct_skill = home / "skills" / "django-expert"
|
|
shutil.copytree(ROOT / "skills" / "django-expert", direct_skill)
|
|
|
|
with self.assertRaises(smoke.SmokeFailure) as raised:
|
|
smoke.smoke_repository(ROOT, "codex", home)
|
|
|
|
self.assertEqual(
|
|
str(raised.exception),
|
|
"duplicate registrations detected: django-expert: "
|
|
"provenance=codex-marketplace,direct-agent-skill; "
|
|
"remediation=keep one installation channel and remove the shadow copy",
|
|
)
|
|
|
|
def test_smoke_uses_catalog_package_path_with_stable_install_id(self):
|
|
source_catalog = self.load_catalog()
|
|
plugin = copy.deepcopy(source_catalog["plugins"][2])
|
|
plugin["package"] = "plugins/cdrf-bundle"
|
|
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
root = Path(temporary) / "repository"
|
|
home = Path(temporary) / "home"
|
|
(root / "plugins").mkdir(parents=True)
|
|
shutil.copytree(
|
|
ROOT / source_catalog["plugins"][2]["package"],
|
|
root / plugin["package"],
|
|
)
|
|
(root / "plugins" / "catalog.json").write_text(
|
|
json.dumps({"plugins": [plugin]})
|
|
)
|
|
|
|
results = smoke.smoke_repository(root, "codex", home)
|
|
|
|
self.assertEqual([result["plugin"] for result in results], ["cdrf-expert"])
|
|
self.assertTrue((home / "plugins" / "cdrf-expert").is_dir())
|
|
self.assertFalse((home / "plugins" / "cdrf-bundle").exists())
|
|
|
|
def test_opencode_smoke_times_out_for_never_resolving_adapter(self):
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
root = Path(temporary) / "repository"
|
|
home = Path(temporary) / "home"
|
|
adapter = root / ".opencode" / "plugins" / "django-ai-skills.js"
|
|
adapter.parent.mkdir(parents=True)
|
|
adapter.write_text(
|
|
"export default async () => ({\n"
|
|
" config: async () => new Promise(() => "
|
|
"{ setInterval(() => {}, 1000) }),\n"
|
|
"})\n"
|
|
)
|
|
skill = root / "skills" / "fixture-skill"
|
|
skill.mkdir(parents=True)
|
|
(skill / "SKILL.md").write_text(
|
|
"---\nname: fixture-skill\ndescription: Fixture.\n---\n"
|
|
)
|
|
(root / "package.json").write_text(
|
|
'{"name":"timeout-fixture","private":true,"type":"module"}\n'
|
|
)
|
|
(root / "plugins").mkdir()
|
|
(root / "plugins" / "catalog.json").write_text(
|
|
json.dumps({"plugins": [{"id": "fixture-skill"}]})
|
|
)
|
|
|
|
with mock.patch.object(smoke, "OPENCODE_TIMEOUT_SECONDS", 0.1):
|
|
with self.assertRaises(smoke.SmokeFailure) as raised:
|
|
smoke.smoke_repository(root, "opencode", home)
|
|
|
|
self.assertIn(
|
|
"django-ai-skills.js' timed out after 0.1 seconds",
|
|
str(raised.exception),
|
|
)
|
|
|
|
def test_layout_versions_are_bumped_for_cache_refresh(self):
|
|
versions = {plugin["version"] for plugin in self.load_catalog()["plugins"]}
|
|
|
|
self.assertEqual(versions, {"1.1.0"})
|
|
|
|
def test_catalog_change_marks_every_native_output_stale(self):
|
|
with tempfile.TemporaryDirectory() as temporary_directory:
|
|
root = Path(temporary_directory)
|
|
(root / "plugins").mkdir()
|
|
shutil.copytree(ROOT / "skills" / "cdrf-expert", root / "skills/cdrf-expert")
|
|
(root / "plugins/cdrf-expert").mkdir()
|
|
source_catalog = self.load_catalog()
|
|
catalog = {
|
|
**{
|
|
key: source_catalog[key]
|
|
for key in (
|
|
"schema_version",
|
|
"repository",
|
|
"defaults",
|
|
"marketplaces",
|
|
)
|
|
},
|
|
"plugins": [
|
|
next(
|
|
plugin
|
|
for plugin in source_catalog["plugins"]
|
|
if plugin["id"] == "cdrf-expert"
|
|
)
|
|
],
|
|
}
|
|
(root / "plugins/catalog.json").write_text(json.dumps(catalog))
|
|
|
|
generator.generate_adapters(root)
|
|
catalog["plugins"][0]["description"] = "Updated fixture description."
|
|
(root / "plugins/catalog.json").write_text(json.dumps(catalog))
|
|
|
|
stale = generator.generate_adapters(root, check=True)
|
|
|
|
self.assertIn(".claude-plugin/marketplace.json", stale)
|
|
self.assertIn(
|
|
"plugins/cdrf-expert/.claude-plugin/plugin.json",
|
|
stale,
|
|
)
|
|
self.assertIn(
|
|
"plugins/cdrf-expert/.codex-plugin/plugin.json",
|
|
stale,
|
|
)
|
|
self.assertIn(
|
|
"plugins/cdrf-expert/.cursor-plugin/plugin.json",
|
|
stale,
|
|
)
|
|
generator.generate_adapters(root)
|
|
self.assertEqual(generator.generate_adapters(root, check=True), [])
|
|
|
|
def test_repository_package_is_private_dependency_free_esm(self):
|
|
package = json.loads((ROOT / "package.json").read_text())
|
|
|
|
self.assertTrue(package["private"])
|
|
self.assertEqual(package["type"], "module")
|
|
self.assertNotIn("dependencies", package)
|
|
self.assertNotIn("devDependencies", package)
|
|
self.assertNotIn("scripts", package)
|
|
|
|
def test_collection_version_uses_semantic_not_lexicographic_order(self):
|
|
versions = ["1.9.0", "1.10.0", "2.0.0"]
|
|
|
|
self.assertEqual(max(versions, key=generator._semver_key), "2.0.0")
|
|
self.assertEqual(
|
|
max(versions[:2], key=generator._semver_key),
|
|
"1.10.0",
|
|
)
|
|
|
|
def test_generic_agent_skills_are_the_five_canonical_directories(self):
|
|
expected = [plugin["id"] for plugin in self.load_catalog()["plugins"]]
|
|
discovered = sorted(path.parent.name for path in (ROOT / "skills").glob("*/SKILL.md"))
|
|
|
|
self.assertEqual(discovered, sorted(expected))
|
|
for skill_id in discovered:
|
|
self.assertEqual(generator.validate_skill(ROOT / "skills" / skill_id), [])
|
|
|
|
def test_removing_catalog_record_prunes_only_generated_package_outputs(self):
|
|
with tempfile.TemporaryDirectory() as temporary_directory:
|
|
root = Path(temporary_directory)
|
|
source_catalog = self.load_catalog()
|
|
selected = [
|
|
plugin
|
|
for plugin in source_catalog["plugins"]
|
|
if plugin["id"] in {"cdrf-expert", "django-safe-migration"}
|
|
]
|
|
for plugin in selected:
|
|
canonical = ROOT / Path(plugin["capability"]["canonical_path"]).parent
|
|
shutil.copytree(
|
|
canonical,
|
|
root / Path(plugin["capability"]["canonical_path"]).parent,
|
|
)
|
|
package = root / plugin["package"]
|
|
package.mkdir(parents=True)
|
|
(package / "README.md").write_text("keep package documentation\n")
|
|
catalog = {
|
|
key: source_catalog[key]
|
|
for key in (
|
|
"schema_version",
|
|
"repository",
|
|
"defaults",
|
|
"marketplaces",
|
|
)
|
|
}
|
|
catalog["plugins"] = selected
|
|
(root / "plugins" / "catalog.json").write_text(json.dumps(catalog))
|
|
generator.generate_adapters(root)
|
|
|
|
removed = catalog["plugins"].pop()
|
|
(root / "plugins" / "catalog.json").write_text(json.dumps(catalog))
|
|
changed = generator.generate_adapters(root)
|
|
removed_package = root / removed["package"]
|
|
|
|
self.assertIn(
|
|
f"{removed['package']}/.cursor-plugin/plugin.json",
|
|
changed,
|
|
)
|
|
self.assertEqual(
|
|
(removed_package / "README.md").read_text(),
|
|
"keep package documentation\n",
|
|
)
|
|
self.assertFalse((removed_package / ".claude-plugin/plugin.json").exists())
|
|
self.assertFalse((removed_package / ".codex-plugin/plugin.json").exists())
|
|
self.assertFalse((removed_package / ".cursor-plugin/plugin.json").exists())
|
|
self.assertFalse(
|
|
(
|
|
removed_package
|
|
/ Path(removed["capability"]["package_path"]).parent
|
|
).exists()
|
|
)
|
|
|
|
def test_complete_sixth_catalog_plugin_generates_every_adapter(self):
|
|
with tempfile.TemporaryDirectory() as temporary_directory:
|
|
root = Path(temporary_directory)
|
|
source_catalog = self.load_catalog()
|
|
base = copy.deepcopy(source_catalog["plugins"][2])
|
|
base.update(
|
|
{
|
|
"id": "fixture-expert",
|
|
"package": "plugins/fixture-expert",
|
|
"description": "Complete fixture plugin.",
|
|
}
|
|
)
|
|
base["capability"].update(
|
|
{
|
|
"canonical_path": "skills/fixture-expert/SKILL.md",
|
|
"package_path": "skills/fixture-expert/SKILL.md",
|
|
}
|
|
)
|
|
base["interface"]["display_name"] = "Fixture Expert"
|
|
skill = root / "skills" / "fixture-expert"
|
|
skill.mkdir(parents=True)
|
|
(skill / "SKILL.md").write_text(
|
|
"---\nname: fixture-expert\n"
|
|
"description: Complete fixture skill.\n---\n\n"
|
|
"Use this fixture for adapter contract tests.\n"
|
|
)
|
|
package = root / base["package"]
|
|
package.mkdir(parents=True)
|
|
catalog = {
|
|
key: source_catalog[key]
|
|
for key in (
|
|
"schema_version",
|
|
"repository",
|
|
"defaults",
|
|
"marketplaces",
|
|
)
|
|
}
|
|
catalog["plugins"] = [base]
|
|
(root / "plugins" / "catalog.json").write_text(json.dumps(catalog))
|
|
|
|
generator.generate_adapters(root)
|
|
|
|
for target in ("claude", "codex", "cursor"):
|
|
self.assertTrue(
|
|
(package / f".{target}-plugin" / "plugin.json").is_file()
|
|
)
|
|
self.assertTrue((package / "skills/fixture-expert/SKILL.md").is_file())
|
|
for marketplace in (
|
|
root / ".claude-plugin/marketplace.json",
|
|
root / ".agents/plugins/marketplace.json",
|
|
root / ".cursor-plugin/marketplace.json",
|
|
):
|
|
names = [
|
|
entry["name"]
|
|
for entry in json.loads(marketplace.read_text())["plugins"]
|
|
]
|
|
self.assertEqual(names, ["fixture-expert"])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|