Files
pulumi__agent-skills/tests/test_manifests.py
T
Mark d083ad2cc8 Add combined pulumi plugin for one-install marketplace distribution (#50)
* Add combined pulumi plugin for one-install marketplace distribution

The Claude marketplace 'pulumi' entry now points at a combined plugin
defined at the repo root, whose skills field bundles the authoring,
migration, and delegation skill groups. A single '/plugin install pulumi'
covers all 14 end-user skills; granular plugins remain for subset
installs. Also adds the official square logo asset for marketplace
listings, raises pulumi-delegation to three Codex starter prompts, and
updates the install docs.

* Review fixes: version 2.0.0 + displayName, doc counts, missing skill listing

Bump the combined plugin to 2.0.0 since it replaces the granular
'pulumi' plugin (also 1.0.0) with different contents, and add a
displayName. Fix the README wording that implied pulumi-package-
maintenance is part of the combined plugin. Correct stale skill counts
(migration 4 to 5, pulumi 7 to 8) and list pulumi-debug-failed-operation
in both docs; it was missing from README and AGENTS.md.

* Reword combined plugin description to say what Pulumi is

* Add HCL to the combined plugin language list

* Extend the combined pulumi plugin to Codex with a root .codex-plugin manifest

The Codex catalog still pointed the pulumi entry at ./pulumi (8 skills)
while the Claude marketplace pointed it at ./ (14 skills), so the same
plugin name installed different contents per ecosystem. Add a root
.codex-plugin/plugin.json mirroring the combined Claude manifest and
point the catalog at ./.

Verified with codex-cli 0.146.0: the skills array registers all 14
skills under the pulumi namespace (checked via codex debug
prompt-input). Manifest tests now cover the root manifests and accept
both string and array skills fields.
2026-08-07 11:31:56 -06:00

106 lines
4.1 KiB
Python

"""
Validate plugin manifests for Claude Code and Codex.
Checks that every plugin.json parses, has the fields each ecosystem requires,
and that marketplace catalogs reference plugin directories that actually exist.
Run with:
uv run pytest tests/test_manifests.py -v
"""
import json
from pathlib import Path
import pytest
REPO_ROOT = Path(__file__).parent.parent
CODEX_REQUIRED_FIELDS = ("name", "version", "description", "skills")
CLAUDE_REQUIRED_FIELDS = ("name", "version", "description")
def _codex_manifests() -> list[Path]:
return sorted(REPO_ROOT.glob(".codex-plugin/plugin.json")) + sorted(
REPO_ROOT.glob("*/.codex-plugin/plugin.json")
)
def _claude_manifests() -> list[Path]:
return sorted(REPO_ROOT.glob(".claude-plugin/plugin.json")) + sorted(
REPO_ROOT.glob("*/.claude-plugin/plugin.json")
)
def _rel(path: Path) -> str:
return str(path.relative_to(REPO_ROOT))
@pytest.mark.parametrize("manifest", _codex_manifests(), ids=_rel)
def test_codex_plugin_manifest(manifest: Path) -> None:
data = json.loads(manifest.read_text())
for field in CODEX_REQUIRED_FIELDS:
assert field in data, f"{_rel(manifest)}: missing field `{field}`"
skills = data["skills"]
for skills_entry in skills if isinstance(skills, list) else [skills]:
skills_path = (manifest.parent.parent / skills_entry.lstrip("./")).resolve()
assert skills_path.is_dir(), (
f"{_rel(manifest)}: skills path `{skills_entry}` does not resolve to a directory"
)
@pytest.mark.parametrize("manifest", _claude_manifests(), ids=_rel)
def test_claude_plugin_manifest(manifest: Path) -> None:
data = json.loads(manifest.read_text())
for field in CLAUDE_REQUIRED_FIELDS:
assert field in data, f"{_rel(manifest)}: missing field `{field}`"
def test_codex_marketplace() -> None:
marketplace = REPO_ROOT / ".agents" / "plugins" / "marketplace.json"
if not marketplace.exists():
pytest.skip("Codex marketplace.json not present")
data = json.loads(marketplace.read_text())
plugins = data.get("plugins", [])
assert plugins, f"{_rel(marketplace)}: no plugins listed"
for entry in plugins:
name = entry["name"]
source = entry["source"]
assert source.get("source") == "local", (
f"{_rel(marketplace)}: plugin {name} uses unsupported source `{source.get('source')}`; "
"this test only validates `local` sources"
)
plugin_dir = (REPO_ROOT / source["path"].lstrip("./")).resolve()
plugin_manifest = plugin_dir / ".codex-plugin" / "plugin.json"
assert plugin_manifest.exists(), (
f"{_rel(marketplace)}: plugin {name} points at `{source['path']}` "
f"but `{plugin_manifest.relative_to(REPO_ROOT)}` does not exist"
)
manifest_name = json.loads(plugin_manifest.read_text())["name"]
assert manifest_name == name, (
f"{_rel(marketplace)}: plugin name `{name}` does not match "
f"`{plugin_manifest.relative_to(REPO_ROOT)}` name `{manifest_name}`"
)
def test_claude_marketplace() -> None:
marketplace = REPO_ROOT / ".claude-plugin" / "marketplace.json"
if not marketplace.exists():
pytest.skip("Claude marketplace.json not present")
data = json.loads(marketplace.read_text())
plugins = data.get("plugins", [])
assert plugins, f"{_rel(marketplace)}: no plugins listed"
for entry in plugins:
name = entry["name"]
source = entry["source"]
plugin_dir = (REPO_ROOT / source.lstrip("./")).resolve()
plugin_manifest = plugin_dir / ".claude-plugin" / "plugin.json"
assert plugin_manifest.exists(), (
f"{_rel(marketplace)}: plugin {name} points at `{source}` "
f"but `{plugin_manifest.relative_to(REPO_ROOT)}` does not exist"
)
manifest_name = json.loads(plugin_manifest.read_text())["name"]
assert manifest_name == name, (
f"{_rel(marketplace)}: plugin name `{name}` does not match "
f"`{plugin_manifest.relative_to(REPO_ROOT)}` name `{manifest_name}`"
)