aissia/tests/mcp/StdioTransportTests.cpp

446 lines
14 KiB
C++

/**
* @file StdioTransportTests.cpp
* @brief Integration tests for StdioTransport (20 TI)
*/
#include <catch2/catch_test_macros.hpp>
#include "shared/mcp/StdioTransport.hpp"
#include "shared/mcp/MCPTypes.hpp"
#include <thread>
#include <chrono>
using namespace aissia::mcp;
using json = nlohmann::json;
// ============================================================================
// Helper: Create config for echo server
// ============================================================================
MCPServerConfig makeEchoServerConfig() {
MCPServerConfig config;
config.name = "echo";
config.command = "C:\\Users\\alexi\\AppData\\Local\\Programs\\Python\\Python312\\python.exe";
config.args = {"tests/fixtures/echo_server.py"};
config.enabled = true;
return config;
}
MCPServerConfig makeMockMCPServerConfig() {
MCPServerConfig config;
config.name = "mock_mcp";
config.command = "C:\\Users\\alexi\\AppData\\Local\\Programs\\Python\\Python312\\python.exe";
config.args = {"tests/fixtures/mock_mcp_server.py"};
config.enabled = true;
return config;
}
// ============================================================================
// TI_TRANSPORT_001: Start spawns process
// ============================================================================
TEST_CASE("TI_TRANSPORT_001_StartSpawnsProcess", "[mcp][transport]") {
auto config = makeEchoServerConfig();
StdioTransport transport(config);
bool started = transport.start();
REQUIRE(started == true);
REQUIRE(transport.isRunning() == true);
transport.stop();
}
// ============================================================================
// TI_TRANSPORT_002: Start fails with invalid command
// ============================================================================
TEST_CASE("TI_TRANSPORT_002_StartFailsInvalidCommand", "[mcp][transport]") {
MCPServerConfig config;
config.name = "invalid";
config.command = "nonexistent_command_xyz";
config.enabled = true;
StdioTransport transport(config);
bool started = transport.start();
REQUIRE(started == false);
REQUIRE(transport.isRunning() == false);
}
// ============================================================================
// TI_TRANSPORT_003: Stop kills process
// ============================================================================
TEST_CASE("TI_TRANSPORT_003_StopKillsProcess", "[mcp][transport]") {
auto config = makeEchoServerConfig();
StdioTransport transport(config);
transport.start();
REQUIRE(transport.isRunning() == true);
transport.stop();
REQUIRE(transport.isRunning() == false);
}
// ============================================================================
// TI_TRANSPORT_004: IsRunning reflects state
// ============================================================================
TEST_CASE("TI_TRANSPORT_004_IsRunningReflectsState", "[mcp][transport]") {
auto config = makeEchoServerConfig();
StdioTransport transport(config);
REQUIRE(transport.isRunning() == false);
transport.start();
REQUIRE(transport.isRunning() == true);
transport.stop();
REQUIRE(transport.isRunning() == false);
}
// ============================================================================
// TI_TRANSPORT_005: SendRequest writes to stdin
// ============================================================================
TEST_CASE("TI_TRANSPORT_005_SendRequestWritesToStdin", "[mcp][transport]") {
auto config = makeEchoServerConfig();
StdioTransport transport(config);
transport.start();
JsonRpcRequest request;
request.id = 1;
request.method = "test";
request.params = {{"message", "hello"}};
// Echo server will echo back params as result
auto response = transport.sendRequest(request, 5000);
// If we got a response, the request was written
REQUIRE(response.isError() == false);
transport.stop();
}
// ============================================================================
// TI_TRANSPORT_006: SendRequest reads response
// ============================================================================
TEST_CASE("TI_TRANSPORT_006_SendRequestReadsResponse", "[mcp][transport]") {
auto config = makeEchoServerConfig();
StdioTransport transport(config);
transport.start();
JsonRpcRequest request;
request.id = 42;
request.method = "echo";
request.params = {{"value", 123}};
auto response = transport.sendRequest(request, 5000);
REQUIRE(response.isError() == false);
REQUIRE(response.id == 42);
REQUIRE(response.result.has_value());
REQUIRE(response.result.value()["value"] == 123);
transport.stop();
}
// ============================================================================
// TI_TRANSPORT_007: SendRequest timeout
// ============================================================================
TEST_CASE("TI_TRANSPORT_007_SendRequestTimeout", "[mcp][transport]") {
// Use cat which doesn't respond to JSON-RPC
MCPServerConfig config;
config.name = "cat";
config.command = "cat";
config.enabled = true;
StdioTransport transport(config);
transport.start();
JsonRpcRequest request;
request.id = 1;
request.method = "test";
// Very short timeout
auto response = transport.sendRequest(request, 100);
// Should timeout and return error
REQUIRE(response.isError() == true);
transport.stop();
}
// ============================================================================
// TI_TRANSPORT_008: SendRequest ID matching
// ============================================================================
TEST_CASE("TI_TRANSPORT_008_SendRequestIdMatching", "[mcp][transport]") {
auto config = makeEchoServerConfig();
StdioTransport transport(config);
transport.start();
// Send request with specific ID
JsonRpcRequest request;
request.id = 999;
request.method = "test";
auto response = transport.sendRequest(request, 5000);
// Response ID should match request ID
REQUIRE(response.id == 999);
transport.stop();
}
// ============================================================================
// TI_TRANSPORT_009: Concurrent requests
// ============================================================================
TEST_CASE("TI_TRANSPORT_009_ConcurrentRequests", "[mcp][transport]") {
auto config = makeEchoServerConfig();
StdioTransport transport(config);
transport.start();
std::vector<std::thread> threads;
std::vector<bool> results(5, false);
for (int i = 0; i < 5; i++) {
threads.emplace_back([&transport, &results, i]() {
JsonRpcRequest request;
request.id = 100 + i;
request.method = "test";
request.params = {{"index", i}};
auto response = transport.sendRequest(request, 5000);
results[i] = !response.isError() && response.id == 100 + i;
});
}
for (auto& t : threads) {
t.join();
}
// All requests should succeed
for (bool result : results) {
REQUIRE(result == true);
}
transport.stop();
}
// ============================================================================
// TI_TRANSPORT_010: SendNotification no response
// ============================================================================
TEST_CASE("TI_TRANSPORT_010_SendNotificationNoResponse", "[mcp][transport]") {
auto config = makeEchoServerConfig();
StdioTransport transport(config);
transport.start();
// Should not block or throw
REQUIRE_NOTHROW(transport.sendNotification("notification/test", {{"data", "value"}}));
transport.stop();
}
// ============================================================================
// TI_TRANSPORT_011: Reader thread starts on start
// ============================================================================
TEST_CASE("TI_TRANSPORT_011_ReaderThreadStartsOnStart", "[mcp][transport]") {
auto config = makeEchoServerConfig();
StdioTransport transport(config);
transport.start();
// If reader thread didn't start, sendRequest would hang
JsonRpcRequest request;
request.id = 1;
request.method = "test";
auto response = transport.sendRequest(request, 1000);
// Got response means reader thread is working
REQUIRE(response.isError() == false);
transport.stop();
}
// ============================================================================
// TI_TRANSPORT_012: Reader thread stops on stop
// ============================================================================
TEST_CASE("TI_TRANSPORT_012_ReaderThreadStopsOnStop", "[mcp][transport]") {
auto config = makeEchoServerConfig();
StdioTransport transport(config);
transport.start();
transport.stop();
// Should not hang or crash on destruction
SUCCEED();
}
// ============================================================================
// TI_TRANSPORT_013: JSON parse error handled
// ============================================================================
TEST_CASE("TI_TRANSPORT_013_JsonParseErrorHandled", "[mcp][transport]") {
auto config = makeEchoServerConfig();
StdioTransport transport(config);
transport.start();
// Send valid request - server will respond with valid JSON
JsonRpcRequest request;
request.id = 1;
request.method = "test";
// Should not crash even if server sends invalid JSON
REQUIRE_NOTHROW(transport.sendRequest(request, 1000));
transport.stop();
}
// ============================================================================
// TI_TRANSPORT_014: Process crash detected
// ============================================================================
TEST_CASE("TI_TRANSPORT_014_ProcessCrashDetected", "[mcp][transport]") {
// TODO: Need a server that crashes to test this
// For now, just verify we can handle stop
auto config = makeEchoServerConfig();
StdioTransport transport(config);
transport.start();
transport.stop();
REQUIRE(transport.isRunning() == false);
}
// ============================================================================
// TI_TRANSPORT_015: Large message handling
// ============================================================================
TEST_CASE("TI_TRANSPORT_015_LargeMessageHandling", "[mcp][transport]") {
auto config = makeEchoServerConfig();
StdioTransport transport(config);
transport.start();
// Create large params
std::string largeString(10000, 'x');
JsonRpcRequest request;
request.id = 1;
request.method = "test";
request.params = {{"data", largeString}};
auto response = transport.sendRequest(request, 10000);
REQUIRE(response.isError() == false);
REQUIRE(response.result.value()["data"] == largeString);
transport.stop();
}
// ============================================================================
// TI_TRANSPORT_016: Multiline JSON handling
// ============================================================================
TEST_CASE("TI_TRANSPORT_016_MultilineJsonHandling", "[mcp][transport]") {
auto config = makeEchoServerConfig();
StdioTransport transport(config);
transport.start();
// JSON with newlines in strings should work
JsonRpcRequest request;
request.id = 1;
request.method = "test";
request.params = {{"text", "line1\nline2\nline3"}};
auto response = transport.sendRequest(request, 5000);
REQUIRE(response.isError() == false);
transport.stop();
}
// ============================================================================
// TI_TRANSPORT_017: Env variables passed to process
// ============================================================================
TEST_CASE("TI_TRANSPORT_017_EnvVariablesPassedToProcess", "[mcp][transport]") {
auto config = makeEchoServerConfig();
config.env["TEST_VAR"] = "test_value";
StdioTransport transport(config);
bool started = transport.start();
REQUIRE(started == true);
transport.stop();
}
// ============================================================================
// TI_TRANSPORT_018: Args passed to process
// ============================================================================
TEST_CASE("TI_TRANSPORT_018_ArgsPassedToProcess", "[mcp][transport]") {
auto config = makeMockMCPServerConfig();
// Args are already set in the helper function
StdioTransport transport(config);
bool started = transport.start();
REQUIRE(started == true);
transport.stop();
}
// ============================================================================
// TI_TRANSPORT_019: Destructor cleans up
// ============================================================================
TEST_CASE("TI_TRANSPORT_019_DestructorCleansUp", "[mcp][transport]") {
{
auto config = makeEchoServerConfig();
StdioTransport transport(config);
transport.start();
// Destructor called here
}
// Should not leak resources or hang
SUCCEED();
}
// ============================================================================
// TI_TRANSPORT_020: Restart after stop
// ============================================================================
TEST_CASE("TI_TRANSPORT_020_RestartAfterStop", "[mcp][transport]") {
auto config = makeEchoServerConfig();
StdioTransport transport(config);
// First start/stop
transport.start();
transport.stop();
REQUIRE(transport.isRunning() == false);
// Second start
bool restarted = transport.start();
REQUIRE(restarted == true);
REQUIRE(transport.isRunning() == true);
// Verify it works
JsonRpcRequest request;
request.id = 1;
request.method = "test";
auto response = transport.sendRequest(request, 5000);
REQUIRE(response.isError() == false);
transport.stop();
}