Import AITURK IDE 1.0.0-beta.1 from Hermes 63279301; preserve MIT license
This commit is contained in:
@@ -0,0 +1,255 @@
|
||||
"""Tests for plugin-registered native platform handler factories.
|
||||
|
||||
Covers:
|
||||
* ``PluginContext.register_platform_handler`` validation + queuing
|
||||
* ``PluginContext.register_telegram_handler`` back-compat alias
|
||||
* ``PluginManager.get_platform_handler_factories`` accessor (+ telegram alias)
|
||||
* ``BasePlatformAdapter._wire_plugin_handlers`` invoking factories with
|
||||
``(native, adapter)`` — exercised through the Telegram adapter
|
||||
* Defensive isolation: a factory that raises does NOT prevent other
|
||||
factories from wiring or the platform from connecting.
|
||||
* Platform scoping: factories for platform A never fire for platform B.
|
||||
* ``discover_and_load(force=True)`` clears queued factories.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Ensure the repo root is importable when this test runs directly
|
||||
# ---------------------------------------------------------------------------
|
||||
_repo = str(Path(__file__).resolve().parents[2])
|
||||
if _repo not in sys.path:
|
||||
sys.path.insert(0, _repo)
|
||||
|
||||
from plugins.platforms.telegram.adapter import TelegramAdapter # noqa: E402
|
||||
from gateway.config import PlatformConfig # noqa: E402
|
||||
|
||||
from hermes_cli.plugins import ( # noqa: E402
|
||||
PluginContext,
|
||||
PluginManager,
|
||||
PluginManifest,
|
||||
)
|
||||
|
||||
|
||||
def _make_ctx(name: str = "test_plugin") -> tuple[PluginManager, PluginContext]:
|
||||
mgr = PluginManager()
|
||||
manifest = PluginManifest(name=name, version="0.1.0", description="test")
|
||||
ctx = PluginContext(manifest=manifest, manager=mgr)
|
||||
return mgr, ctx
|
||||
|
||||
|
||||
def _make_adapter() -> TelegramAdapter:
|
||||
config = PlatformConfig(enabled=True, token="test-token", extra={})
|
||||
adapter = TelegramAdapter(config)
|
||||
adapter._app = MagicMock()
|
||||
adapter._bot = MagicMock()
|
||||
return adapter
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# PluginContext.register_platform_handler — validation + queuing
|
||||
# ===========================================================================
|
||||
|
||||
class TestRegisterPlatformHandlerAPI:
|
||||
def test_factory_is_queued_with_plugin_name(self):
|
||||
mgr, ctx = _make_ctx()
|
||||
|
||||
def factory(native, adapter): # pragma: no cover - never called
|
||||
pass
|
||||
|
||||
ctx.register_platform_handler("discord", factory)
|
||||
|
||||
factories = mgr.get_platform_handler_factories("discord")
|
||||
assert len(factories) == 1
|
||||
fn, plugin_name = factories[0]
|
||||
assert fn is factory
|
||||
assert plugin_name == "test_plugin"
|
||||
|
||||
def test_platform_key_is_normalized(self):
|
||||
mgr, ctx = _make_ctx()
|
||||
ctx.register_platform_handler(" Slack ", lambda n, a: None)
|
||||
assert len(mgr.get_platform_handler_factories("slack")) == 1
|
||||
|
||||
def test_non_callable_factory_raises(self):
|
||||
_, ctx = _make_ctx()
|
||||
with pytest.raises(ValueError, match="non-callable"):
|
||||
ctx.register_platform_handler("discord", "nope") # type: ignore[arg-type]
|
||||
|
||||
def test_empty_platform_raises(self):
|
||||
_, ctx = _make_ctx()
|
||||
with pytest.raises(ValueError, match="empty platform"):
|
||||
ctx.register_platform_handler(" ", lambda n, a: None)
|
||||
|
||||
def test_platform_scoping(self):
|
||||
"""Factories for platform A never appear in platform B's list."""
|
||||
mgr, ctx = _make_ctx()
|
||||
ctx.register_platform_handler("discord", lambda n, a: None)
|
||||
ctx.register_platform_handler("matrix", lambda n, a: None)
|
||||
assert len(mgr.get_platform_handler_factories("discord")) == 1
|
||||
assert len(mgr.get_platform_handler_factories("matrix")) == 1
|
||||
assert mgr.get_platform_handler_factories("slack") == []
|
||||
|
||||
def test_accessor_returns_copy(self):
|
||||
mgr, ctx = _make_ctx()
|
||||
ctx.register_platform_handler("telegram", lambda n, a: None)
|
||||
|
||||
got = mgr.get_platform_handler_factories("telegram")
|
||||
got.append(("junk", "junk"))
|
||||
assert len(mgr.get_platform_handler_factories("telegram")) == 1
|
||||
|
||||
def test_multiple_plugins_each_recorded(self):
|
||||
mgr = PluginManager()
|
||||
for name in ("plugin_a", "plugin_b"):
|
||||
manifest = PluginManifest(name=name, version="0.1.0", description="t")
|
||||
ctx = PluginContext(manifest=manifest, manager=mgr)
|
||||
ctx.register_platform_handler("telegram", lambda n, a: None)
|
||||
|
||||
names = [n for _, n in mgr.get_platform_handler_factories("telegram")]
|
||||
assert names == ["plugin_a", "plugin_b"]
|
||||
|
||||
def test_force_rediscovery_clears_factories(self):
|
||||
mgr, ctx = _make_ctx()
|
||||
ctx.register_platform_handler("telegram", lambda n, a: None)
|
||||
assert len(mgr.get_platform_handler_factories("telegram")) == 1
|
||||
|
||||
mgr.discover_and_load(force=True)
|
||||
assert mgr.get_platform_handler_factories("telegram") == []
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# Telegram back-compat alias
|
||||
# ===========================================================================
|
||||
|
||||
class TestTelegramAlias:
|
||||
def test_register_telegram_handler_routes_to_telegram_bucket(self):
|
||||
mgr, ctx = _make_ctx()
|
||||
|
||||
def factory(application, adapter): # pragma: no cover
|
||||
pass
|
||||
|
||||
ctx.register_telegram_handler(factory)
|
||||
|
||||
assert mgr.get_platform_handler_factories("telegram") == [
|
||||
(factory, "test_plugin")
|
||||
]
|
||||
# Legacy accessor still works.
|
||||
assert mgr.get_telegram_handler_factories() == [(factory, "test_plugin")]
|
||||
|
||||
def test_alias_non_callable_raises(self):
|
||||
_, ctx = _make_ctx()
|
||||
with pytest.raises(ValueError, match="non-callable"):
|
||||
ctx.register_telegram_handler("not-a-callable") # type: ignore[arg-type]
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# BasePlatformAdapter._wire_plugin_handlers (via TelegramAdapter)
|
||||
# ===========================================================================
|
||||
|
||||
class TestAdapterPluginWiring:
|
||||
def test_factory_invoked_with_native_and_adapter(self):
|
||||
adapter = _make_adapter()
|
||||
calls = []
|
||||
|
||||
def factory(native, adp):
|
||||
calls.append((native, adp))
|
||||
native.add_handler(MagicMock())
|
||||
|
||||
mgr = MagicMock()
|
||||
mgr.get_platform_handler_factories.return_value = [(factory, "biz_plugin")]
|
||||
|
||||
with patch("hermes_cli.plugins.get_plugin_manager", return_value=mgr):
|
||||
adapter._wire_plugin_handlers(adapter._app)
|
||||
|
||||
assert calls == [(adapter._app, adapter)]
|
||||
adapter._app.add_handler.assert_called_once()
|
||||
# Adapter asked for its own platform's factories.
|
||||
mgr.get_platform_handler_factories.assert_called_once_with("telegram")
|
||||
|
||||
def test_no_factories_is_a_noop(self):
|
||||
adapter = _make_adapter()
|
||||
mgr = MagicMock()
|
||||
mgr.get_platform_handler_factories.return_value = []
|
||||
|
||||
with patch("hermes_cli.plugins.get_plugin_manager", return_value=mgr):
|
||||
adapter._wire_plugin_handlers(adapter._app)
|
||||
|
||||
adapter._app.add_handler.assert_not_called()
|
||||
|
||||
def test_raising_factory_does_not_block_others(self):
|
||||
adapter = _make_adapter()
|
||||
wired = []
|
||||
|
||||
def bad_factory(native, adp):
|
||||
raise RuntimeError("boom")
|
||||
|
||||
def good_factory(native, adp):
|
||||
wired.append("good")
|
||||
|
||||
mgr = MagicMock()
|
||||
mgr.get_platform_handler_factories.return_value = [
|
||||
(bad_factory, "bad_plugin"),
|
||||
(good_factory, "good_plugin"),
|
||||
]
|
||||
|
||||
with patch("hermes_cli.plugins.get_plugin_manager", return_value=mgr):
|
||||
adapter._wire_plugin_handlers(adapter._app) # must not raise
|
||||
|
||||
assert wired == ["good"]
|
||||
|
||||
def test_manager_load_failure_does_not_raise(self):
|
||||
adapter = _make_adapter()
|
||||
with patch(
|
||||
"hermes_cli.plugins.get_plugin_manager",
|
||||
side_effect=RuntimeError("plugin system down"),
|
||||
):
|
||||
adapter._wire_plugin_handlers(adapter._app) # must not raise
|
||||
|
||||
def test_native_none_supported(self):
|
||||
"""Adapters without a separate native client pass None."""
|
||||
adapter = _make_adapter()
|
||||
seen = []
|
||||
|
||||
mgr = MagicMock()
|
||||
mgr.get_platform_handler_factories.return_value = [
|
||||
(lambda native, adp: seen.append(native), "p"),
|
||||
]
|
||||
with patch("hermes_cli.plugins.get_plugin_manager", return_value=mgr):
|
||||
adapter._wire_plugin_handlers(None)
|
||||
assert seen == [None]
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# Every adapter calls _wire_plugin_handlers in connect() — source invariant
|
||||
# ===========================================================================
|
||||
|
||||
def test_all_connectable_adapters_wire_plugin_handlers():
|
||||
"""Invariant: every platform adapter with a connect() implementation
|
||||
calls ``_wire_plugin_handlers`` somewhere in its source, so plugins can
|
||||
rely on the hook existing on every platform (native may be None)."""
|
||||
import glob
|
||||
|
||||
repo = Path(_repo)
|
||||
adapter_files = sorted(
|
||||
glob.glob(str(repo / "plugins" / "platforms" / "*" / "adapter.py"))
|
||||
) + [
|
||||
str(repo / "gateway" / "platforms" / name)
|
||||
for name in (
|
||||
"api_server.py", "bluebubbles.py", "msgraph_webhook.py",
|
||||
"qqbot/adapter.py", "signal.py", "webhook.py", "weixin.py",
|
||||
"whatsapp_cloud.py", "yuanbao.py",
|
||||
)
|
||||
]
|
||||
missing = []
|
||||
for f in adapter_files:
|
||||
src = Path(f).read_text()
|
||||
if "async def connect(" not in src:
|
||||
continue
|
||||
if "_wire_plugin_handlers" not in src:
|
||||
missing.append(f)
|
||||
assert not missing, f"adapters missing plugin-handler wiring: {missing}"
|
||||
Reference in New Issue
Block a user