codex-agent-3 / tests /test_toolservers.py
m5ike's picture
up3
c1a62a8
Raw
History Blame Contribute Delete
7.31 kB
"""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}