Files
hermes-agent/tests/tui_gateway/test_slash_worker_mcp_discovery.py
elphamale 48470f8628 test(mcp): port the slash-worker discovery probe to MCPServer
`tests/tui_gateway/test_slash_worker_mcp_discovery.py` gated on
`pytest.importorskip("mcp.server.fastmcp")` and generated a probe server
that imported `FastMCP`. Since this branch removes that module by pinning
mcp 2.x, the whole file skipped instead of running — and a skip caused by
our own dependency change reads as a pass in a per-file sweep, which is how
it got missed.

The test is real end-to-end coverage of the path this branch migrates: it
spawns an MCP server subprocess, registers it as a profile-local
`mcp_servers` entry, drives `/tools` through the slash worker, and asserts
the prefixed tool name is discovered. Port the probe to
`mcp.server.MCPServer` (`run(transport="stdio")` is unchanged) so it
exercises the migrated client instead of skipping.

The module-level gate now checks for `MCPServer` specifically, so a
FastMCP-era SDK still skips cleanly rather than failing on the 2.x-only
probe.

Verified it now executes rather than skips: 1 passed in 5.37s, versus
"1 skipped in 1.04s" before.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 23:26:10 -07:00

115 lines
3.3 KiB
Python

"""Integration coverage for profile-local MCP discovery in slash workers."""
from __future__ import annotations
import json
import os
from pathlib import Path
import queue
import subprocess
import sys
import textwrap
import threading
import pytest
import yaml
_mcp_server_mod = pytest.importorskip("mcp.server")
if not hasattr(_mcp_server_mod, "MCPServer"):
# `mcp.server.MCPServer` replaced `mcp.server.fastmcp.FastMCP` in mcp 2.0.
# Skip rather than fail on a FastMCP-era SDK: the probe below is written
# against the 2.x API, and the pinned version provides it.
pytest.skip(
"profile-local MCP discovery probe requires mcp >= 2.0 (MCPServer)",
allow_module_level=True,
)
def test_profile_local_mcp_tool_is_visible_in_slash_worker(tmp_path):
profile_home = tmp_path / "profile-home"
profile_home.mkdir()
marker = "profile-local-61922"
server = tmp_path / "mcp_probe.py"
server.write_text(
textwrap.dedent(
f"""
from mcp.server import MCPServer
mcp = MCPServer("profileprobe")
@mcp.tool()
def hermes_61922_profile_probe() -> str:
return {marker!r}
if __name__ == "__main__":
mcp.run(transport="stdio")
"""
),
encoding="utf-8",
)
(profile_home / "config.yaml").write_text(
yaml.safe_dump(
{
"mcp_servers": {
"profileprobe": {
"enabled": True,
"command": sys.executable,
"args": [str(server)],
}
}
}
),
encoding="utf-8",
)
env = os.environ.copy()
for key in list(env):
if key.endswith("_API_KEY") or key.endswith("_TOKEN"):
env.pop(key)
env["HERMES_HOME"] = str(profile_home)
env["PYTHONPATH"] = str(Path(__file__).resolve().parents[2])
env["HERMES_SLASH_WATCHDOG_GRACE_S"] = "0"
env["HERMES_SLASH_WATCHDOG_POLL_S"] = "0.05"
proc = subprocess.Popen(
[
sys.executable,
"-u",
"-m",
"tui_gateway.slash_worker",
"--session-key",
"agent:main:tui:dm:mcp-profile-test",
],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
env=env,
cwd=tmp_path,
)
output: queue.Queue[str] = queue.Queue()
try:
assert proc.stdin is not None
assert proc.stdout is not None
stdout = proc.stdout
threading.Thread(
target=lambda: output.put(stdout.readline()),
daemon=True,
).start()
proc.stdin.write(json.dumps({"id": 1, "command": "/tools"}) + "\n")
proc.stdin.flush()
try:
line = output.get(timeout=10)
except queue.Empty:
pytest.fail("slash worker produced no /tools response within 10 seconds")
response = json.loads(line)
assert response["ok"] is True
assert "mcp__profileprobe__hermes_61922_profile_probe" in response["output"]
finally:
proc.terminate()
try:
proc.wait(timeout=5)
except subprocess.TimeoutExpired:
proc.kill()
proc.wait(timeout=5)