Spaces:
Paused
Paused
File size: 7,313 Bytes
c1a62a8 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 | """Testy externích tool serverů (MCP klient + manager + integrace)."""
import json
import pytest
from fastapi.testclient import TestClient
from tests.mock_mcp import MockMCPServer
@pytest.fixture()
def mcp():
server = MockMCPServer()
yield server
server.stop()
@pytest.fixture()
def mcp_sse():
server = MockMCPServer(sse=True)
yield server
server.stop()
@pytest.fixture()
def ts_module(env):
import importlib
import sys
for name in ("presets", "settings", "toolservers"):
sys.modules.pop(name, None)
return importlib.import_module("toolservers")
# ---------------------------------------------------------------- parsování
def test_parse_sse_response(ts_module):
body = ('event: message\n'
'data: {"jsonrpc":"2.0","id":1,"result":{"ok":true}}\n\n')
data = ts_module.parse_mcp_response("text/event-stream", body)
assert data["result"] == {"ok": True}
data = ts_module.parse_mcp_response("application/json",
'{"jsonrpc":"2.0","id":1,"result":{}}')
assert data["result"] == {}
with pytest.raises(ValueError):
ts_module.parse_mcp_response("text/event-stream", "data: nevalidni\n")
def test_parse_registry_payload(ts_module):
payload = {"servers": [
{"server": {"name": "a/b", "description": "x" * 300, "version": "1.0",
"remotes": [{"type": "streamable-http", "url": "https://a/mcp"}]}},
{"server": {"name": "c/d", "description": "local only",
"packages": [{"registryType": "npm"}]}},
]}
out = ts_module.parse_registry_payload(payload)
assert out[0]["urls"] == ["https://a/mcp"] and out[0]["remote"] is True
assert len(out[0]["description"]) == 200
assert out[1]["remote"] is False
# ---------------------------------------------------------------- manager
def test_discover_and_call(ts_module, mcp):
mgr = ts_module.ToolServerManager()
mgr.add_or_update("Mock-Server", mcp.url)
servers = mgr.list()
assert servers[0]["name"] == "mock_server"
assert servers[0]["status"] == "ok"
assert servers[0]["tool_names"] == ["echo", "add"]
tools = mgr.openai_tools()
names = [t["function"]["name"] for t in tools]
assert names == ["ext_mock_server_echo", "ext_mock_server_add"]
assert tools[0]["function"]["parameters"]["required"] == ["text"]
assert "[externí nástroj" in tools[0]["function"]["description"]
res = mgr.call("ext_mock_server_echo", {"text": "ahoj"})
assert res == {"result": "echo:ahoj"}
res = mgr.call("ext_mock_server_add", {"a": 2, "b": 3})
assert res["structured"] == {"sum": 5}
def test_sse_transport(ts_module, mcp_sse):
mgr = ts_module.ToolServerManager()
mgr.add_or_update("ssemock", mcp_sse.url)
assert mgr.list()[0]["status"] == "ok"
assert mgr.call("ext_ssemock_echo", {"text": "x"}) == {"result": "echo:x"}
def test_auth_token(ts_module):
server = MockMCPServer(token="tajny")
try:
mgr = ts_module.ToolServerManager()
mgr.add_or_update("secured", server.url) # bez tokenu
assert mgr.list()[0]["status"] == "error"
mgr.add_or_update("secured", server.url, token="tajny")
assert mgr.list()[0]["status"] == "ok"
# maskovaný token z UI nepřepíše skutečný
mgr.add_or_update("secured", server.url, token="********")
assert mgr.list()[0]["status"] == "ok"
finally:
server.stop()
def test_allowed_tools_filter_and_disable(ts_module, mcp):
mgr = ts_module.ToolServerManager()
mgr.add_or_update("mock", mcp.url, allowed_tools="echo")
names = [t["function"]["name"] for t in mgr.openai_tools()]
assert names == ["ext_mock_echo"]
assert "error" in mgr.call("ext_mock_add", {"a": 1, "b": 1})
mgr.add_or_update("mock", mcp.url, enabled=False, refresh=False)
assert mgr.openai_tools() == []
assert "error" in mgr.call("ext_mock_echo", {"text": "x"})
def test_tool_error_result(ts_module, mcp):
mgr = ts_module.ToolServerManager()
mgr.add_or_update("mock", mcp.url)
# ruční route na neexistující nástroj serveru
mgr._routing["ext_mock_ghost"] = ("mock", "ghost")
res = mgr.call("ext_mock_ghost", {})
assert "error" in res and "unknown tool" in res["error"]
def test_persistence_roundtrip(ts_module, mcp):
mgr = ts_module.ToolServerManager()
mgr.add_or_update("mock", mcp.url, token="tajny", allowed_tools="echo")
mgr2 = ts_module.ToolServerManager(path=mgr.path)
row = mgr2.list()[0]
assert row["name"] == "mock" and row["allowed_tools"] == "echo"
assert row["token"] == "********" # maskováno v list()
assert row["tool_names"] == ["echo", "add"] # cache nástrojů přežila
# routing funguje i po restartu (lazy klient si udělá initialize)
assert mgr2.call("ext_mock_echo", {"text": "hi"}) == {"result": "echo:hi"}
def test_validation(ts_module):
mgr = ts_module.ToolServerManager()
with pytest.raises(ValueError):
mgr.add_or_update("x", "ftp://spatne")
with pytest.raises(ValueError):
mgr.add_or_update("###", "https://ok/mcp", refresh=False)
# ---------------------------------------------------------------- integrace
def test_exec_tool_routing_and_role_guard(app_module, mcp):
app_module.TOOL_SERVERS.add_or_update("mock", mcp.url)
# hlavní agent (má "external") smí
res = app_module._exec_tool("ext_mock_echo", {"text": "hej"},
app_module.MAIN_ALLOWED)
assert res == {"result": "echo:hej"}
# sub-agent bez "external" nesmí
res = app_module._exec_tool("ext_mock_echo", {"text": "hej"},
app_module.READONLY)
assert "nejsou v této roli povoleny" in res["error"]
# nástroje se objeví v hlavních tools
names = [t["function"]["name"] for t in app_module.build_main_tools()]
assert "ext_mock_echo" in names and "ext_mock_add" in names
def test_admin_api_crud(app_module, auth, mcp):
c = TestClient(app_module.app)
assert c.get("/admin/toolservers").status_code == 401
r = c.post("/admin/toolservers", headers=auth,
json={"name": "mock", "url": mcp.url})
assert r.status_code == 200
assert r.json()["summary"]["tools"] == 2
r = c.get("/admin/toolservers", headers=auth)
assert r.json()["servers"][0]["status"] == "ok"
r = c.post("/admin/toolservers/refresh", headers=auth, json={"name": "mock"})
assert "ok" in r.json()["results"]["mock"]
r = c.get("/admin/toolservers/catalog", headers=auth)
assert any(s["name"] == "context7" for s in r.json()["servers"])
r = c.post("/admin/toolservers", headers=auth,
json={"name": "bad", "url": "ftp://x"})
assert r.status_code == 400
r = c.post("/admin/toolservers/delete", headers=auth, json={"name": "mock"})
assert r.status_code == 200
assert c.get("/admin/toolservers", headers=auth).json()["servers"] == []
def test_health_reports_toolservers(app_module, auth, mcp):
app_module.TOOL_SERVERS.add_or_update("mock", mcp.url)
c = TestClient(app_module.app)
data = c.get("/health").json()
assert data["tool_servers"] == {"servers": 1, "enabled": 1, "tools": 2}
|