| 1 | import json |
| 2 | import tomllib |
| 3 | import unittest |
| 4 | from pathlib import Path |
| 5 | |
| 6 | from lib.skill_meta import read_skill_version |
| 7 | |
| 8 | ROOT = Path(__file__).resolve().parents[1] |
| 9 | SKILL_ROOT = ROOT / "skills" / "last30days" |
| 10 | |
| 11 | |
| 12 | def _json(path: Path) -> dict: |
| 13 | return json.loads(path.read_text(encoding="utf-8")) |
| 14 | |
| 15 | |
| 16 | def _skill_version() -> str: |
| 17 | version = read_skill_version(SKILL_ROOT / "SKILL.md") |
| 18 | if not version: |
| 19 | raise AssertionError("SKILL.md version frontmatter not found") |
| 20 | return version |
| 21 | |
| 22 | |
| 23 | class TestPluginContract(unittest.TestCase): |
| 24 | def test_codex_plugin_manifest_uses_repo_skill_root(self) -> None: |
| 25 | manifest = _json(ROOT / ".codex-plugin" / "plugin.json") |
| 26 | |
| 27 | self.assertEqual("last30days", manifest["name"]) |
| 28 | self.assertEqual("./skills/", manifest["skills"]) |
| 29 | self.assertEqual("last30days", manifest["interface"]["displayName"]) |
| 30 | |
| 31 | def test_codex_marketplace_points_at_repo_root_plugin(self) -> None: |
| 32 | marketplace = _json(ROOT / ".agents" / "plugins" / "marketplace.json") |
| 33 | plugins = marketplace.get("plugins") or [] |
| 34 | plugin_by_name = {plugin["name"]: plugin for plugin in plugins} |
| 35 | |
| 36 | self.assertEqual("last30days-skill", marketplace["name"]) |
| 37 | self.assertIn("last30days", plugin_by_name) |
| 38 | plugin = plugin_by_name["last30days"] |
| 39 | self.assertEqual( |
| 40 | { |
| 41 | "source": "url", |
| 42 | "url": "https://github.com/mvanhorn/last30days-skill.git", |
| 43 | }, |
| 44 | plugin["source"], |
| 45 | ) |
| 46 | |
| 47 | def test_grok_plugin_manifest_uses_repo_skill_root(self) -> None: |
| 48 | manifest = _json(ROOT / ".grok-plugin" / "plugin.json") |
| 49 | |
| 50 | self.assertEqual("last30days", manifest["name"]) |
| 51 | self.assertEqual("./skills/", manifest["skills"]) |
| 52 | |
| 53 | def test_grok_marketplace_points_at_repo_root_plugin(self) -> None: |
| 54 | marketplace = _json(ROOT / ".grok-plugin" / "marketplace.json") |
| 55 | plugins = marketplace.get("plugins") or [] |
| 56 | plugin_by_name = {plugin["name"]: plugin for plugin in plugins} |
| 57 | |
| 58 | self.assertEqual("last30days-skill", marketplace["name"]) |
| 59 | self.assertIn("last30days", plugin_by_name) |
| 60 | plugin = plugin_by_name["last30days"] |
| 61 | # Exact dict equality locks the bare Git URL source (anti-self-referential-local). |
| 62 | self.assertEqual( |
| 63 | { |
| 64 | "source": "url", |
| 65 | "url": "https://github.com/mvanhorn/last30days-skill.git", |
| 66 | }, |
| 67 | plugin["source"], |
| 68 | ) |
| 69 | |
| 70 | def test_versions_match_across_manifests(self) -> None: |
| 71 | pyproject = tomllib.loads((ROOT / "pyproject.toml").read_text(encoding="utf-8")) |
| 72 | version = pyproject["project"]["version"] |
| 73 | |
| 74 | self.assertEqual(version, _skill_version()) |
| 75 | self.assertEqual(version, _json(ROOT / ".claude-plugin" / "plugin.json")["version"]) |
| 76 | self.assertEqual(version, _json(ROOT / ".codex-plugin" / "plugin.json")["version"]) |
| 77 | self.assertEqual(version, _json(ROOT / ".grok-plugin" / "plugin.json")["version"]) |
| 78 | self.assertEqual(version, _json(ROOT / "gemini-extension.json")["version"]) |
| 79 | |
| 80 | marketplace = _json(ROOT / ".claude-plugin" / "marketplace.json") |
| 81 | plugins = marketplace.get("plugins") or [] |
| 82 | self.assertEqual(1, len(plugins)) |
| 83 | self.assertEqual(version, plugins[0]["version"]) |
| 84 | |
| 85 | grok_marketplace = _json(ROOT / ".grok-plugin" / "marketplace.json") |
| 86 | grok_plugins = grok_marketplace.get("plugins") or [] |
| 87 | self.assertEqual(1, len(grok_plugins)) |
| 88 | self.assertEqual(version, grok_plugins[0]["version"]) |
| 89 | |
| 90 | def test_claude_marketplace_has_current_schema_shape(self) -> None: |
| 91 | marketplace = _json(ROOT / ".claude-plugin" / "marketplace.json") |
| 92 | |
| 93 | self.assertNotIn("$schema", marketplace) |
| 94 | self.assertNotIn("description", marketplace) |
| 95 | self.assertIn("metadata", marketplace) |
| 96 | self.assertIn("description", marketplace["metadata"]) |
| 97 | |
| 98 | def test_grok_marketplace_has_current_schema_shape(self) -> None: |
| 99 | marketplace = _json(ROOT / ".grok-plugin" / "marketplace.json") |
| 100 | |
| 101 | self.assertNotIn("$schema", marketplace) |
| 102 | self.assertNotIn("metadata", marketplace) |
| 103 | self.assertIsInstance(marketplace["description"], str) |
| 104 | self.assertIn("name", marketplace) |
| 105 | self.assertIn("owner", marketplace) |
| 106 | self.assertIn("plugins", marketplace) |
| 107 | |
| 108 | def test_workflows_do_not_reference_removed_root_scripts_dir(self) -> None: |
| 109 | # The historical root-level scripts/ directory was removed; workflows must not |
| 110 | # reference a bare `scripts/` path. Allowed replacements: |
| 111 | # skills/last30days/scripts/ (engine), mcp/scripts/ (.mcpb), .github/scripts/ |
| 112 | # (release automation). |
| 113 | allowed_prefixes = ( |
| 114 | "skills/last30days/scripts/", |
| 115 | "mcp/scripts/", |
| 116 | ".github/scripts/", |
| 117 | ) |
| 118 | offenders = [] |
| 119 | for path in sorted((ROOT / ".github" / "workflows").glob("*.yml")): |
| 120 | for line_number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1): |
| 121 | if "scripts/" not in line: |
| 122 | continue |
| 123 | if any(prefix in line for prefix in allowed_prefixes): |
| 124 | continue |
| 125 | offenders.append(f"{path.relative_to(ROOT)}:{line_number}: {line.strip()}") |
| 126 | |
| 127 | self.assertEqual([], offenders) |
| 128 | |
| 129 | if __name__ == "__main__": |
| 130 | unittest.main() |
| 131 |