134 lines
5.2 KiB
Python
134 lines
5.2 KiB
Python
"""Tests for the hermes-tools-as-MCP server module surface.
|
|
|
|
We don't run a live MCP session in unit tests — that requires the codex
|
|
subprocess + client + an event loop. These tests pin the static
|
|
contract: the module imports, the EXPOSED_TOOLS list is sane, and the
|
|
build helper assembles a server when the SDK is present.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
class TestModuleSurface:
|
|
def test_module_imports_clean(self):
|
|
from agent.transports import hermes_tools_mcp_server as m
|
|
assert callable(m.main)
|
|
assert callable(m._build_server)
|
|
assert isinstance(m.EXPOSED_TOOLS, tuple)
|
|
assert len(m.EXPOSED_TOOLS) > 0
|
|
|
|
def test_exposed_tools_are_safe_subset(self):
|
|
"""We MUST NOT expose tools codex already has, because codex'
|
|
own builtins are better-integrated with its sandbox + approvals.
|
|
Specifically: no terminal/shell, no read_file/write_file, no
|
|
patch — those are codex's built-in tools."""
|
|
from agent.transports.hermes_tools_mcp_server import EXPOSED_TOOLS
|
|
forbidden = {
|
|
"terminal", "shell", "read_file", "write_file", "patch",
|
|
"search_files", "process",
|
|
}
|
|
leaked = forbidden & set(EXPOSED_TOOLS)
|
|
assert not leaked, (
|
|
f"these tools must NOT be exposed via the codex callback "
|
|
f"because codex has built-in equivalents: {leaked}"
|
|
)
|
|
|
|
def test_expected_hermes_specific_tools_listed(self):
|
|
"""The Hermes-specific tools should be present so users on the
|
|
codex runtime keep access to them."""
|
|
from agent.transports.hermes_tools_mcp_server import EXPOSED_TOOLS
|
|
for required in (
|
|
"web_search",
|
|
"web_extract",
|
|
"browser_navigate",
|
|
"vision_analyze",
|
|
"image_generate",
|
|
"skill_view",
|
|
):
|
|
assert required in EXPOSED_TOOLS, f"missing {required!r}"
|
|
|
|
def test_agent_loop_tools_not_exposed(self):
|
|
"""delegate_task / memory / session_search / todo require the
|
|
running AIAgent context to dispatch, so a stateless MCP callback
|
|
can't drive them. They must NOT be in EXPOSED_TOOLS."""
|
|
from agent.transports.hermes_tools_mcp_server import EXPOSED_TOOLS
|
|
for agent_loop_tool in ("delegate_task", "memory", "session_search", "todo"):
|
|
assert agent_loop_tool not in EXPOSED_TOOLS, (
|
|
f"{agent_loop_tool!r} requires the agent loop context "
|
|
"and can't be reached through a stateless MCP callback"
|
|
)
|
|
|
|
def test_kanban_worker_tools_exposed(self):
|
|
"""Kanban workers run as `hermes chat -q` subprocesses; if they
|
|
come up on the codex_app_server runtime, the worker can do the
|
|
actual work via codex's shell but needs the kanban tools through
|
|
the MCP callback to report back to the kernel. Without these
|
|
tools available, the worker would hang at completion time."""
|
|
from agent.transports.hermes_tools_mcp_server import EXPOSED_TOOLS
|
|
# Worker handoff tools — every dispatched worker uses at least
|
|
# one of {complete, block, comment} to close out its task.
|
|
for worker_tool in (
|
|
"kanban_complete",
|
|
"kanban_block",
|
|
"kanban_comment",
|
|
"kanban_heartbeat",
|
|
):
|
|
assert worker_tool in EXPOSED_TOOLS, (
|
|
f"{worker_tool!r} missing from codex callback — kanban "
|
|
"workers on codex_app_server runtime would hang"
|
|
)
|
|
|
|
def test_kanban_orchestrator_tools_exposed(self):
|
|
"""Orchestrator agents need to dispatch new tasks, query the
|
|
board, and unblock/link tasks. Exposed so an orchestrator on
|
|
codex_app_server can do its job."""
|
|
from agent.transports.hermes_tools_mcp_server import EXPOSED_TOOLS
|
|
for orch_tool in (
|
|
"kanban_create",
|
|
"kanban_show",
|
|
"kanban_list",
|
|
"kanban_unblock",
|
|
"kanban_link",
|
|
):
|
|
assert orch_tool in EXPOSED_TOOLS, (
|
|
f"{orch_tool!r} missing from codex callback"
|
|
)
|
|
|
|
|
|
class TestMain:
|
|
def test_main_returns_2_when_mcp_unavailable(self, monkeypatch):
|
|
"""When the mcp package isn't installed, main() should exit
|
|
cleanly with code 2 and an install hint, not crash."""
|
|
import agent.transports.hermes_tools_mcp_server as m
|
|
|
|
def boom_build(*a, **kw):
|
|
raise ImportError("mcp not installed")
|
|
|
|
monkeypatch.setattr(m, "_build_server", boom_build)
|
|
rc = m.main(["--verbose"])
|
|
assert rc == 2
|
|
|
|
def test_main_handles_keyboard_interrupt(self, monkeypatch):
|
|
import agent.transports.hermes_tools_mcp_server as m
|
|
|
|
class FakeServer:
|
|
def run(self):
|
|
raise KeyboardInterrupt()
|
|
|
|
monkeypatch.setattr(m, "_build_server", lambda: FakeServer())
|
|
rc = m.main([])
|
|
assert rc == 0
|
|
|
|
def test_main_returns_1_on_runtime_error(self, monkeypatch):
|
|
import agent.transports.hermes_tools_mcp_server as m
|
|
|
|
class CrashingServer:
|
|
def run(self):
|
|
raise RuntimeError("boom")
|
|
|
|
monkeypatch.setattr(m, "_build_server", lambda: CrashingServer())
|
|
rc = m.main([])
|
|
assert rc == 1
|