393 lines
12 KiB
C++
393 lines
12 KiB
C++
/**
|
|
* @file MCPClientTests.cpp
|
|
* @brief Integration tests for MCPClient (15 TI)
|
|
*/
|
|
|
|
#include <catch2/catch_test_macros.hpp>
|
|
#include "shared/mcp/MCPClient.hpp"
|
|
#include "mocks/MockTransport.hpp"
|
|
|
|
#include <fstream>
|
|
#include <filesystem>
|
|
|
|
using namespace aissia::mcp;
|
|
using namespace aissia::tests;
|
|
using json = nlohmann::json;
|
|
|
|
// ============================================================================
|
|
// Helper: Create test config file
|
|
// ============================================================================
|
|
|
|
std::string createTestConfigFile(const json& config) {
|
|
std::string path = "test_mcp_config.json";
|
|
std::ofstream file(path);
|
|
file << config.dump(2);
|
|
file.close();
|
|
return path;
|
|
}
|
|
|
|
void cleanupTestConfigFile(const std::string& path) {
|
|
std::filesystem::remove(path);
|
|
}
|
|
|
|
// ============================================================================
|
|
// TI_CLIENT_001: Load config valid
|
|
// ============================================================================
|
|
|
|
TEST_CASE("TI_CLIENT_001_LoadConfigValid", "[mcp][client]") {
|
|
json config = {
|
|
{"servers", {
|
|
{"test_server", {
|
|
{"command", "C:\\Users\\alexi\\AppData\\Local\\Programs\\Python\\Python312\\python.exe"},
|
|
{"args", json::array({"server.py"})},
|
|
{"enabled", true}
|
|
}}
|
|
}}
|
|
};
|
|
auto path = createTestConfigFile(config);
|
|
|
|
MCPClient client;
|
|
bool loaded = client.loadConfig(path);
|
|
|
|
REQUIRE(loaded == true);
|
|
|
|
cleanupTestConfigFile(path);
|
|
}
|
|
|
|
// ============================================================================
|
|
// TI_CLIENT_002: Load config invalid
|
|
// ============================================================================
|
|
|
|
TEST_CASE("TI_CLIENT_002_LoadConfigInvalid", "[mcp][client]") {
|
|
// Create file with invalid JSON
|
|
std::string path = "invalid_config.json";
|
|
std::ofstream file(path);
|
|
file << "{ invalid json }";
|
|
file.close();
|
|
|
|
MCPClient client;
|
|
bool loaded = client.loadConfig(path);
|
|
|
|
REQUIRE(loaded == false);
|
|
|
|
cleanupTestConfigFile(path);
|
|
}
|
|
|
|
// ============================================================================
|
|
// TI_CLIENT_003: Load config missing file
|
|
// ============================================================================
|
|
|
|
TEST_CASE("TI_CLIENT_003_LoadConfigMissingFile", "[mcp][client]") {
|
|
MCPClient client;
|
|
bool loaded = client.loadConfig("nonexistent_file.json");
|
|
|
|
REQUIRE(loaded == false);
|
|
}
|
|
|
|
// ============================================================================
|
|
// TI_CLIENT_004: ConnectAll starts servers
|
|
// ============================================================================
|
|
|
|
TEST_CASE("TI_CLIENT_004_ConnectAllStartsServers", "[mcp][client]") {
|
|
// Use the real mock MCP server fixture
|
|
MCPClient client;
|
|
bool loaded = client.loadConfig("tests/fixtures/mock_mcp.json");
|
|
|
|
if (loaded) {
|
|
int connected = client.connectAll();
|
|
// Should connect to enabled servers
|
|
REQUIRE(connected >= 0);
|
|
client.disconnectAll();
|
|
} else {
|
|
// Skip if fixture not available
|
|
SUCCEED();
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// TI_CLIENT_005: ConnectAll skips disabled
|
|
// ============================================================================
|
|
|
|
TEST_CASE("TI_CLIENT_005_ConnectAllSkipsDisabled", "[mcp][client]") {
|
|
json config = {
|
|
{"servers", {
|
|
{"enabled_server", {
|
|
{"command", "C:\\Users\\alexi\\AppData\\Local\\Programs\\Python\\Python312\\python.exe"},
|
|
{"args", json::array({"tests/fixtures/echo_server.py"})},
|
|
{"enabled", true}
|
|
}},
|
|
{"disabled_server", {
|
|
{"command", "nonexistent"},
|
|
{"enabled", false}
|
|
}}
|
|
}}
|
|
};
|
|
auto path = createTestConfigFile(config);
|
|
|
|
MCPClient client;
|
|
client.loadConfig(path);
|
|
int connected = client.connectAll();
|
|
|
|
// disabled_server should not be connected
|
|
REQUIRE(client.isConnected("disabled_server") == false);
|
|
|
|
client.disconnectAll();
|
|
cleanupTestConfigFile(path);
|
|
}
|
|
|
|
// ============================================================================
|
|
// TI_CLIENT_006: Connect single server
|
|
// ============================================================================
|
|
|
|
TEST_CASE("TI_CLIENT_006_ConnectSingleServer", "[mcp][client]") {
|
|
json config = {
|
|
{"servers", {
|
|
{"server1", {
|
|
{"command", "C:\\Users\\alexi\\AppData\\Local\\Programs\\Python\\Python312\\python.exe"},
|
|
{"args", json::array({"tests/fixtures/echo_server.py"})},
|
|
{"enabled", true}
|
|
}},
|
|
{"server2", {
|
|
{"command", "C:\\Users\\alexi\\AppData\\Local\\Programs\\Python\\Python312\\python.exe"},
|
|
{"args", json::array({"tests/fixtures/echo_server.py"})},
|
|
{"enabled", true}
|
|
}}
|
|
}}
|
|
};
|
|
auto path = createTestConfigFile(config);
|
|
|
|
MCPClient client;
|
|
client.loadConfig(path);
|
|
|
|
// Connect only server1
|
|
bool connected = client.connect("server1");
|
|
|
|
REQUIRE(connected == true);
|
|
REQUIRE(client.isConnected("server1") == true);
|
|
REQUIRE(client.isConnected("server2") == false);
|
|
|
|
client.disconnectAll();
|
|
cleanupTestConfigFile(path);
|
|
}
|
|
|
|
// ============================================================================
|
|
// TI_CLIENT_007: Disconnect single server
|
|
// ============================================================================
|
|
|
|
TEST_CASE("TI_CLIENT_007_DisconnectSingleServer", "[mcp][client]") {
|
|
json config = {
|
|
{"servers", {
|
|
{"server1", {
|
|
{"command", "C:\\Users\\alexi\\AppData\\Local\\Programs\\Python\\Python312\\python.exe"},
|
|
{"args", json::array({"tests/fixtures/echo_server.py"})},
|
|
{"enabled", true}
|
|
}}
|
|
}}
|
|
};
|
|
auto path = createTestConfigFile(config);
|
|
|
|
MCPClient client;
|
|
client.loadConfig(path);
|
|
client.connect("server1");
|
|
REQUIRE(client.isConnected("server1") == true);
|
|
|
|
client.disconnect("server1");
|
|
REQUIRE(client.isConnected("server1") == false);
|
|
|
|
cleanupTestConfigFile(path);
|
|
}
|
|
|
|
// ============================================================================
|
|
// TI_CLIENT_008: DisconnectAll cleans up
|
|
// ============================================================================
|
|
|
|
TEST_CASE("TI_CLIENT_008_DisconnectAllCleansUp", "[mcp][client]") {
|
|
json config = {
|
|
{"servers", {
|
|
{"server1", {
|
|
{"command", "C:\\Users\\alexi\\AppData\\Local\\Programs\\Python\\Python312\\python.exe"},
|
|
{"args", json::array({"tests/fixtures/echo_server.py"})},
|
|
{"enabled", true}
|
|
}},
|
|
{"server2", {
|
|
{"command", "C:\\Users\\alexi\\AppData\\Local\\Programs\\Python\\Python312\\python.exe"},
|
|
{"args", json::array({"tests/fixtures/echo_server.py"})},
|
|
{"enabled", true}
|
|
}}
|
|
}}
|
|
};
|
|
auto path = createTestConfigFile(config);
|
|
|
|
MCPClient client;
|
|
client.loadConfig(path);
|
|
client.connectAll();
|
|
|
|
client.disconnectAll();
|
|
|
|
REQUIRE(client.isConnected("server1") == false);
|
|
REQUIRE(client.isConnected("server2") == false);
|
|
REQUIRE(client.getConnectedServers().empty() == true);
|
|
|
|
cleanupTestConfigFile(path);
|
|
}
|
|
|
|
// ============================================================================
|
|
// TI_CLIENT_009: ListAllTools aggregates
|
|
// ============================================================================
|
|
|
|
TEST_CASE("TI_CLIENT_009_ListAllToolsAggregates", "[mcp][client]") {
|
|
MCPClient client;
|
|
bool loaded = client.loadConfig("tests/fixtures/mock_mcp.json");
|
|
|
|
if (loaded) {
|
|
client.connectAll();
|
|
auto tools = client.listAllTools();
|
|
|
|
// Should have tools from mock server
|
|
REQUIRE(tools.size() >= 0);
|
|
|
|
client.disconnectAll();
|
|
} else {
|
|
SUCCEED();
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// TI_CLIENT_010: Tool name prefixed
|
|
// ============================================================================
|
|
|
|
TEST_CASE("TI_CLIENT_010_ToolNamePrefixed", "[mcp][client]") {
|
|
MCPClient client;
|
|
bool loaded = client.loadConfig("tests/fixtures/mock_mcp.json");
|
|
|
|
if (loaded) {
|
|
client.connectAll();
|
|
auto tools = client.listAllTools();
|
|
|
|
bool hasPrefix = false;
|
|
for (const auto& tool : tools) {
|
|
if (tool.name.find(":") != std::string::npos) {
|
|
hasPrefix = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!tools.empty()) {
|
|
REQUIRE(hasPrefix == true);
|
|
}
|
|
|
|
client.disconnectAll();
|
|
} else {
|
|
SUCCEED();
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// TI_CLIENT_011: CallTool routes to server
|
|
// ============================================================================
|
|
|
|
TEST_CASE("TI_CLIENT_011_CallToolRoutesToServer", "[mcp][client]") {
|
|
MCPClient client;
|
|
bool loaded = client.loadConfig("tests/fixtures/mock_mcp.json");
|
|
|
|
if (loaded) {
|
|
client.connectAll();
|
|
auto tools = client.listAllTools();
|
|
|
|
if (!tools.empty()) {
|
|
// Call the first available tool
|
|
auto result = client.callTool(tools[0].name, json::object());
|
|
// Should get some result (success or error)
|
|
REQUIRE(result.content.size() >= 0);
|
|
}
|
|
|
|
client.disconnectAll();
|
|
} else {
|
|
SUCCEED();
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// TI_CLIENT_012: CallTool invalid name
|
|
// ============================================================================
|
|
|
|
TEST_CASE("TI_CLIENT_012_CallToolInvalidName", "[mcp][client]") {
|
|
MCPClient client;
|
|
client.loadConfig("tests/fixtures/mock_mcp.json");
|
|
client.connectAll();
|
|
|
|
auto result = client.callTool("nonexistent:tool", json::object());
|
|
|
|
REQUIRE(result.isError == true);
|
|
|
|
client.disconnectAll();
|
|
}
|
|
|
|
// ============================================================================
|
|
// TI_CLIENT_013: CallTool disconnected server
|
|
// ============================================================================
|
|
|
|
TEST_CASE("TI_CLIENT_013_CallToolDisconnectedServer", "[mcp][client]") {
|
|
MCPClient client;
|
|
// Don't connect any servers
|
|
|
|
auto result = client.callTool("server:tool", json::object());
|
|
|
|
REQUIRE(result.isError == true);
|
|
}
|
|
|
|
// ============================================================================
|
|
// TI_CLIENT_014: ToolCount accurate
|
|
// ============================================================================
|
|
|
|
TEST_CASE("TI_CLIENT_014_ToolCountAccurate", "[mcp][client]") {
|
|
MCPClient client;
|
|
bool loaded = client.loadConfig("tests/fixtures/mock_mcp.json");
|
|
|
|
if (loaded) {
|
|
client.connectAll();
|
|
|
|
size_t count = client.toolCount();
|
|
auto tools = client.listAllTools();
|
|
|
|
REQUIRE(count == tools.size());
|
|
|
|
client.disconnectAll();
|
|
} else {
|
|
SUCCEED();
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// TI_CLIENT_015: IsConnected accurate
|
|
// ============================================================================
|
|
|
|
TEST_CASE("TI_CLIENT_015_IsConnectedAccurate", "[mcp][client]") {
|
|
json config = {
|
|
{"servers", {
|
|
{"test_server", {
|
|
{"command", "C:\\Users\\alexi\\AppData\\Local\\Programs\\Python\\Python312\\python.exe"},
|
|
{"args", json::array({"tests/fixtures/echo_server.py"})},
|
|
{"enabled", true}
|
|
}}
|
|
}}
|
|
};
|
|
auto path = createTestConfigFile(config);
|
|
|
|
MCPClient client;
|
|
client.loadConfig(path);
|
|
|
|
// Not connected yet
|
|
REQUIRE(client.isConnected("test_server") == false);
|
|
|
|
// Connect
|
|
client.connect("test_server");
|
|
REQUIRE(client.isConnected("test_server") == true);
|
|
|
|
// Disconnect
|
|
client.disconnect("test_server");
|
|
REQUIRE(client.isConnected("test_server") == false);
|
|
|
|
cleanupTestConfigFile(path);
|
|
}
|