Spaces:
Paused
Paused
| """Testy externích tool serverů (MCP klient + manager + integrace).""" | |
| import json | |
| import pytest | |
| from fastapi.testclient import TestClient | |
| from tests.mock_mcp import MockMCPServer | |
| def mcp(): | |
| server = MockMCPServer() | |
| yield server | |
| server.stop() | |
| def mcp_sse(): | |
| server = MockMCPServer(sse=True) | |
| yield server | |
| server.stop() | |
| 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} | |