/** * @file StdioTransportTests.cpp * @brief Integration tests for StdioTransport (20 TI) */ #include #include "shared/mcp/StdioTransport.hpp" #include "shared/mcp/MCPTypes.hpp" #include #include 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 threads; std::vector 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(); }