| 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_mcp_manifest_version_matches_lockstep(self) -> None: |
| 91 | pyproject = tomllib.loads((ROOT / "pyproject.toml").read_text(encoding="utf-8")) |
| 92 | version = pyproject["project"]["version"] |
| 93 | mcp_version = _json(ROOT / "mcp" / "manifest.json")["version"] |
| 94 | |
| 95 | # mcp/manifest.json joined the lockstep set after it had already |
| 96 | # stalled at 3.6.0. Feature PRs may not bump it; only the next release |
| 97 | # PR (prepare_release.py) moves it off 3.6.0, at which point this |
| 98 | # branch is dead and the equality below is enforced unconditionally. |
| 99 | # Any other mismatch is fresh drift and fails. |
| 100 | if mcp_version == "3.6.0" and version != "3.6.0": |
| 101 | self.skipTest( |
| 102 | "mcp/manifest.json is at pre-lockstep 3.6.0; the next release PR " |
| 103 | "bumps it via prepare_release.py" |
| 104 | ) |
| 105 | self.assertEqual(version, mcp_version) |
| 106 | |
| 107 | def test_claude_marketplace_has_current_schema_shape(self) -> None: |
| 108 | marketplace = _json(ROOT / ".claude-plugin" / "marketplace.json") |
| 109 | |
| 110 | self.assertNotIn("$schema", marketplace) |
| 111 | self.assertNotIn("description", marketplace) |
| 112 | self.assertIn("metadata", marketplace) |
| 113 | self.assertIn("description", marketplace["metadata"]) |
| 114 | |
| 115 | def test_grok_marketplace_has_current_schema_shape(self) -> None: |
| 116 | marketplace = _json(ROOT / ".grok-plugin" / "marketplace.json") |
| 117 | |
| 118 | self.assertNotIn("$schema", marketplace) |
| 119 | self.assertNotIn("metadata", marketplace) |
| 120 | self.assertIsInstance(marketplace["description"], str) |
| 121 | self.assertIn("name", marketplace) |
| 122 | self.assertIn("owner", marketplace) |
| 123 | self.assertIn("plugins", marketplace) |
| 124 | |
| 125 | def test_workflows_do_not_reference_removed_root_scripts_dir(self) -> None: |
| 126 | # The historical root-level scripts/ directory was removed; workflows must not |
| 127 | # reference a bare `scripts/` path. Allowed replacements: |
| 128 | # skills/last30days/scripts/ (engine), mcp/scripts/ (.mcpb), .github/scripts/ |
| 129 | # (release automation). |
| 130 | allowed_prefixes = ( |
| 131 | "skills/last30days/scripts/", |
| 132 | "mcp/scripts/", |
| 133 | ".github/scripts/", |
| 134 | ) |
| 135 | offenders = [] |
| 136 | for path in sorted((ROOT / ".github" / "workflows").glob("*.yml")): |
| 137 | for line_number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1): |
| 138 | if "scripts/" not in line: |
| 139 | continue |
| 140 | if any(prefix in line for prefix in allowed_prefixes): |
| 141 | continue |
| 142 | offenders.append(f"{path.relative_to(ROOT)}:{line_number}: {line.strip()}") |
| 143 | |
| 144 | self.assertEqual([], offenders) |
| 145 | |
| 146 | def test_plugin_ships_no_session_start_hook(self) -> None: |
| 147 | # SessionStart ran in every Claude Code / Grok session whether or not |
| 148 | # /last30days was invoked. First-run NUX lives in SKILL.md Step 0. |
| 149 | self.assertFalse((ROOT / "hooks" / "hooks.json").exists()) |
| 150 | self.assertFalse((ROOT / "hooks" / "scripts" / "check-config.sh").exists()) |
| 151 | |
| 152 | def test_mcp_manifest_credential_entries_are_sensitive(self) -> None: |
| 153 | # Every user_config entry that names a credential (title ends in _KEY |
| 154 | # or _TOKEN, or _PASSWORD) must be marked sensitive so Claude Desktop |
| 155 | # stores it in the OS keychain instead of plain config. |
| 156 | manifest = _json(ROOT / "mcp" / "manifest.json") |
| 157 | user_config = manifest["user_config"] |
| 158 | self.assertTrue(user_config) |
| 159 | offenders = [ |
| 160 | key |
| 161 | for key, entry in user_config.items() |
| 162 | if str(entry.get("title", "")).endswith(("_KEY", "_TOKEN", "_PASSWORD")) |
| 163 | and entry.get("sensitive") is not True |
| 164 | ] |
| 165 | self.assertEqual([], offenders) |
| 166 | |
| 167 | def test_mcp_manifest_declares_x_bearer_token(self) -> None: |
| 168 | # U6/R17: the X API bearer is installable through the MCP bundle like |
| 169 | # every other credential, wired user_config -> env under the |
| 170 | # lowercased-env-var convention mcp/internal/manifest enforces. |
| 171 | manifest = _json(ROOT / "mcp" / "manifest.json") |
| 172 | entry = manifest["user_config"]["x_bearer_token"] |
| 173 | self.assertEqual("X_BEARER_TOKEN", entry["title"]) |
| 174 | self.assertEqual("string", entry["type"]) |
| 175 | self.assertIs(True, entry["sensitive"]) |
| 176 | self.assertIs(False, entry["required"]) |
| 177 | self.assertIn("X API v2", entry["description"]) |
| 178 | self.assertIn("bearer", entry["description"].lower()) |
| 179 | self.assertIn("developer.x.com", entry["description"]) |
| 180 | env_map = manifest["server"]["mcp_config"]["env"] |
| 181 | self.assertEqual("${user_config.x_bearer_token}", env_map["X_BEARER_TOKEN"]) |
| 182 | |
| 183 | def test_mcp_manifest_env_and_user_config_cross_reference(self) -> None: |
| 184 | # Python-side mirror of mcp/internal/manifest/manifest_test.go so the |
| 185 | # invariant is checked by `uv run pytest` too. |
| 186 | manifest = _json(ROOT / "mcp" / "manifest.json") |
| 187 | env_map = manifest["server"]["mcp_config"]["env"] |
| 188 | user_config = manifest["user_config"] |
| 189 | for env_name, value in env_map.items(): |
| 190 | self.assertEqual(f"${{user_config.{env_name.lower()}}}", value) |
| 191 | self.assertIn(env_name.lower(), user_config) |
| 192 | for key in user_config: |
| 193 | self.assertIn(key.upper(), env_map) |
| 194 | |
| 195 | if __name__ == "__main__": |
| 196 | unittest.main() |
| 197 |