446 lines
14 KiB
C++
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();
|
|
}
|