|
|
@@ -102,6 +102,41 @@ _SEEN_CAP = ChattoConstants.SEEN_CAP
|
|
|
# -- Helpers --
|
|
|
|
|
|
|
|
|
+def _manifest_provides_tools() -> list[str]:
|
|
|
+ """The tool names plugin.yaml declares as ``provides_tools``."""
|
|
|
+ import yaml
|
|
|
+
|
|
|
+ with open(os.path.join(PLUGIN_ROOT, "plugin.yaml")) as manifest_file:
|
|
|
+ manifest = yaml.safe_load(manifest_file)
|
|
|
+ return list(manifest.get("provides_tools") or [])
|
|
|
+
|
|
|
+
|
|
|
+def _registered_tool_names(ctx) -> list[str]:
|
|
|
+ """Only the tools — ``registered_names`` also carries the platform entry."""
|
|
|
+ return [tool["name"] for tool in ctx.registered_tools]
|
|
|
+
|
|
|
+
|
|
|
+def _load_plugin_tools():
|
|
|
+ """Load the plugin's ``tools.py`` under a private module name.
|
|
|
+
|
|
|
+ Imported bare it would shadow Hermes' own top-level ``tools`` package,
|
|
|
+ which shares this test suite's ``sys.path``.
|
|
|
+ """
|
|
|
+ import importlib.util
|
|
|
+
|
|
|
+ path = os.path.join(PLUGIN_ROOT, "tools.py")
|
|
|
+ if not os.path.isfile(path):
|
|
|
+ pytest.fail(
|
|
|
+ "plugin.yaml declares provides_tools, which Hermes delivers through a "
|
|
|
+ "tools.py — but the plugin ships no tools.py, so CLI/TUI sessions get "
|
|
|
+ "the tools only as a warning log"
|
|
|
+ )
|
|
|
+ spec = importlib.util.spec_from_file_location("chatto_plugin_tools", path)
|
|
|
+ module = importlib.util.module_from_spec(spec)
|
|
|
+ spec.loader.exec_module(module)
|
|
|
+ return module
|
|
|
+
|
|
|
+
|
|
|
class _MockPluginContext:
|
|
|
"""Minimal mock for the plugin registration context."""
|
|
|
|
|
|
@@ -378,6 +413,29 @@ class TestRegistration:
|
|
|
with patch.object(ChattoAdapter, "send_video", BasePlatformAdapter.send_video):
|
|
|
assert "video" not in _capabilities()
|
|
|
|
|
|
+ def test_provides_tools_is_deliverable_in_a_cli_session(self):
|
|
|
+ """``provides_tools`` asks Hermes to register the tools without the
|
|
|
+ adapter, which it can only do through a ``tools.py``. A manifest that
|
|
|
+ promises them there and ships no such module leaves a CLI session with a
|
|
|
+ warning and no tools at all — so the two must agree, name for name.
|
|
|
+ """
|
|
|
+ declared = _manifest_provides_tools()
|
|
|
+ ctx = _MockPluginContext()
|
|
|
+ _load_plugin_tools().register_tools(ctx)
|
|
|
+ assert sorted(_registered_tool_names(ctx)) == sorted(declared)
|
|
|
+
|
|
|
+ def test_both_entry_points_register_the_same_tools(self):
|
|
|
+ """``register`` (gateway/web) and ``register_tools`` (CLI/TUI) are two
|
|
|
+ doors into one list of tools. Anything they disagree on is a tool that
|
|
|
+ exists in half the sessions."""
|
|
|
+ gateway_ctx = _MockPluginContext()
|
|
|
+ register(gateway_ctx)
|
|
|
+ cli_ctx = _MockPluginContext()
|
|
|
+ _load_plugin_tools().register_tools(cli_ctx)
|
|
|
+ assert sorted(_registered_tool_names(cli_ctx)) == sorted(
|
|
|
+ _registered_tool_names(gateway_ctx)
|
|
|
+ )
|
|
|
+
|
|
|
def test_check_requirements(self):
|
|
|
assert check_requirements() is True
|
|
|
|