返回 last30days-skill
test_plugin_contract.py
根目录 / tests / test_plugin_contract.py
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
197 lines PYTHON