| import asyncio |
| import json |
| import unittest |
| |
| from gerrit_mcp_server.plugin_registry import PluginRegistry, requires_plugin |
| |
| |
| def _run(coro): |
| return asyncio.run(coro) |
| |
| |
| def _make_registry(plugins_by_host: dict, ttl: float = 300.0) -> PluginRegistry: |
| async def run_curl(args, base_url): |
| return json.dumps(plugins_by_host.get(base_url, {})) |
| |
| config = {"gerrit_hosts": [{"external_url": url} for url in plugins_by_host]} |
| return PluginRegistry( |
| run_curl=run_curl, |
| normalize_url=lambda u: u.rstrip("/"), |
| load_config=lambda: config, |
| ttl_seconds=ttl, |
| ) |
| |
| |
| class TestPluginRegistry(unittest.TestCase): |
| def test_host_has_plugin_true(self): |
| reg = _make_registry({"https://host-a": {"task": {"version": "1.0"}}}) |
| self.assertTrue(_run(reg.host_has_plugin("https://host-a", "task"))) |
| |
| def test_host_has_plugin_false(self): |
| reg = _make_registry({"https://host-a": {}}) |
| self.assertFalse(_run(reg.host_has_plugin("https://host-a", "task"))) |
| |
| def test_disabled_plugin_excluded(self): |
| reg = _make_registry({"https://host-a": {"task": {"disabled": True}}}) |
| self.assertFalse(_run(reg.host_has_plugin("https://host-a", "task"))) |
| |
| def test_hosts_with_plugin(self): |
| reg = _make_registry( |
| { |
| "https://host-a": {"task": {}}, |
| "https://host-b": {}, |
| } |
| ) |
| hosts = _run(reg.hosts_with_plugin("task")) |
| self.assertIn("https://host-a", hosts) |
| self.assertNotIn("https://host-b", hosts) |
| |
| def test_ttl_cache_hit(self): |
| call_count = 0 |
| |
| async def run_curl(args, base_url): |
| nonlocal call_count |
| call_count += 1 |
| return json.dumps({"task": {}}) |
| |
| reg = PluginRegistry( |
| run_curl=run_curl, |
| normalize_url=lambda u: u, |
| load_config=lambda: {"gerrit_hosts": [{"external_url": "https://host-a"}]}, |
| ttl_seconds=300.0, |
| ) |
| _run(reg.host_has_plugin("https://host-a", "task")) |
| _run(reg.host_has_plugin("https://host-a", "task")) |
| self.assertEqual(call_count, 1) |
| |
| def test_invalidate_single_host(self): |
| call_count = 0 |
| |
| async def run_curl(args, base_url): |
| nonlocal call_count |
| call_count += 1 |
| return json.dumps({"task": {}}) |
| |
| reg = PluginRegistry( |
| run_curl=run_curl, |
| normalize_url=lambda u: u, |
| load_config=lambda: {"gerrit_hosts": [{"external_url": "https://host-a"}]}, |
| ttl_seconds=300.0, |
| ) |
| _run(reg.host_has_plugin("https://host-a", "task")) |
| reg.invalidate("https://host-a") |
| self.assertNotIn("https://host-a", reg._cache) |
| self.assertNotIn("https://host-a", reg._locks) |
| |
| def test_invalidate_all(self): |
| reg = _make_registry( |
| { |
| "https://host-a": {"task": {}}, |
| "https://host-b": {"task": {}}, |
| } |
| ) |
| _run(reg.hosts_with_plugin("task")) |
| reg.invalidate() |
| self.assertEqual(reg._cache, {}) |
| self.assertEqual(reg._locks, {}) |
| |
| def test_plugin_version(self): |
| reg = _make_registry({"https://host-a": {"task": {"version": "2.3"}}}) |
| v = _run(reg.plugin_version("https://host-a", "task")) |
| self.assertEqual(v, "2.3") |
| |
| def test_plugin_version_missing(self): |
| reg = _make_registry({"https://host-a": {}}) |
| v = _run(reg.plugin_version("https://host-a", "task")) |
| self.assertIsNone(v) |
| |
| |
| class TestRequiresPlugin(unittest.TestCase): |
| def _make_reg(self, has_plugin: bool) -> PluginRegistry: |
| async def run_curl(args, base_url): |
| return json.dumps({"task": {}} if has_plugin else {}) |
| |
| return PluginRegistry( |
| run_curl=run_curl, |
| normalize_url=lambda u: u, |
| load_config=lambda: {"gerrit_hosts": [{"external_url": "https://host-a"}]}, |
| ) |
| |
| def test_routes_to_available_host(self): |
| reg = self._make_reg(has_plugin=True) |
| received_url = [] |
| |
| @requires_plugin("task", reg) |
| async def my_tool(gerrit_base_url=None): |
| received_url.append(gerrit_base_url) |
| return "ok" |
| |
| result = _run(my_tool()) |
| self.assertEqual(result, "ok") |
| self.assertEqual(received_url, ["https://host-a"]) |
| |
| def test_soft_hint_when_no_host_has_plugin(self): |
| reg = self._make_reg(has_plugin=False) |
| |
| @requires_plugin("task", reg) |
| async def my_tool(gerrit_base_url=None): |
| return "ok" |
| |
| with self.assertRaises(RuntimeError) as ctx: |
| _run(my_tool()) |
| self.assertIn("task", str(ctx.exception)) |
| |
| def test_soft_hint_when_explicit_host_lacks_plugin(self): |
| reg = self._make_reg(has_plugin=False) |
| |
| @requires_plugin("task", reg) |
| async def my_tool(gerrit_base_url=None): |
| return "ok" |
| |
| with self.assertRaises(RuntimeError) as ctx: |
| _run(my_tool(gerrit_base_url="https://host-a")) |
| self.assertIn("not installed", str(ctx.exception)) |
| |
| def test_passes_through_when_host_has_plugin(self): |
| reg = self._make_reg(has_plugin=True) |
| |
| @requires_plugin("task", reg) |
| async def my_tool(gerrit_base_url=None): |
| return "done" |
| |
| result = _run(my_tool(gerrit_base_url="https://host-a")) |
| self.assertEqual(result, "done") |
| |
| |
| if __name__ == "__main__": |
| unittest.main() |