using Xunit; using Moq; using System; using System.IO; using System.Text; using System.Text.Json; using System.Threading.Tasks; namespace OpenHarbor.MCP.Core.Tests; /// /// Unit tests for StdioTransport following TDD approach. /// Tests JSON-RPC 2.0 protocol over stdin/stdout communication. /// public class StdioTransportTests { [Fact] public void StdioTransport_ShouldInitializeWithStreams() { // Arrange var inputStream = new MemoryStream(); var outputStream = new MemoryStream(); // Act var transport = new StdioTransport(inputStream, outputStream); // Assert Assert.NotNull(transport); } [Fact] public void StdioTransport_ShouldThrowOnNullInputStream() { // Arrange var outputStream = new MemoryStream(); // Act & Assert Assert.Throws(() => new StdioTransport(null!, outputStream) ); } [Fact] public void StdioTransport_ShouldThrowOnNullOutputStream() { // Arrange var inputStream = new MemoryStream(); // Act & Assert Assert.Throws(() => new StdioTransport(inputStream, null!) ); } [Fact] public async Task StdioTransport_ReadRequestAsync_ShouldParseValidJsonRpcRequest() { // Arrange var requestJson = """ {"jsonrpc":"2.0","method":"tools/list","id":"1"} """; // Newline-delimited var inputStream = new MemoryStream(Encoding.UTF8.GetBytes(requestJson)); var outputStream = new MemoryStream(); var transport = new StdioTransport(inputStream, outputStream); // Act var request = await transport.ReadRequestAsync(); // Assert Assert.NotNull(request); Assert.Equal("2.0", request.JsonRpc); Assert.Equal("tools/list", request.Method); Assert.Equal("1", request.Id); } [Fact] public async Task StdioTransport_ReadRequestAsync_WithParams_ShouldParseParams() { // Arrange var requestJson = """ {"jsonrpc":"2.0","method":"tools/call","id":"2","params":{"name":"search_codex","arguments":{"query":"test"}}} """; var inputStream = new MemoryStream(Encoding.UTF8.GetBytes(requestJson)); var outputStream = new MemoryStream(); var transport = new StdioTransport(inputStream, outputStream); // Act var request = await transport.ReadRequestAsync(); // Assert Assert.NotNull(request); Assert.NotNull(request.Params); Assert.True(request.Params.RootElement.TryGetProperty("name", out var name)); Assert.Equal("search_codex", name.GetString()); } [Fact] public async Task StdioTransport_ReadRequestAsync_WithEndOfStream_ShouldReturnNull() { // Arrange var inputStream = new MemoryStream(); // Empty stream var outputStream = new MemoryStream(); var transport = new StdioTransport(inputStream, outputStream); // Act var request = await transport.ReadRequestAsync(); // Assert Assert.Null(request); } [Fact] public async Task StdioTransport_WriteResponseAsync_ShouldWriteValidJsonRpcResponse() { // Arrange var inputStream = new MemoryStream(); var outputStream = new MemoryStream(); var transport = new StdioTransport(inputStream, outputStream); var response = new McpResponse { Id = "1", Result = JsonDocument.Parse("""{"tools":[]}""") }; // Act await transport.WriteResponseAsync(response); // Assert outputStream.Position = 0; var outputText = Encoding.UTF8.GetString(outputStream.ToArray()); Assert.Contains("\"jsonrpc\":\"2.0\"", outputText); Assert.Contains("\"id\":\"1\"", outputText); Assert.Contains("\"result\"", outputText); Assert.EndsWith("\n", outputText); // Should end with newline } [Fact] public async Task StdioTransport_WriteResponseAsync_WithError_ShouldWriteErrorResponse() { // Arrange var inputStream = new MemoryStream(); var outputStream = new MemoryStream(); var transport = new StdioTransport(inputStream, outputStream); var response = new McpResponse { Id = "1", Error = new McpError { Code = -32601, Message = "Method not found" } }; // Act await transport.WriteResponseAsync(response); // Assert outputStream.Position = 0; var outputText = Encoding.UTF8.GetString(outputStream.ToArray()); Assert.Contains("\"error\"", outputText); Assert.Contains("\"code\":-32601", outputText); Assert.Contains("Method not found", outputText); Assert.DoesNotContain("\"result\"", outputText); } [Fact] public async Task StdioTransport_WriteResponseAsync_ShouldFlushOutput() { // Arrange var inputStream = new MemoryStream(); var outputStream = new MemoryStream(); var transport = new StdioTransport(inputStream, outputStream); var response = new McpResponse { Id = "1", Result = JsonDocument.Parse("""{"status":"ok"}""") }; // Act await transport.WriteResponseAsync(response); // Assert - data should be immediately available in stream outputStream.Position = 0; Assert.True(outputStream.Length > 0); } [Fact] public async Task StdioTransport_ReadRequestAsync_WithInvalidJson_ShouldThrowJsonException() { // Arrange var invalidJson = "not valid json{{{"; var inputStream = new MemoryStream(Encoding.UTF8.GetBytes(invalidJson + "\n")); var outputStream = new MemoryStream(); var transport = new StdioTransport(inputStream, outputStream); // Act & Assert await Assert.ThrowsAsync(async () => await transport.ReadRequestAsync() ); } [Fact] public async Task StdioTransport_ReadRequestAsync_MultipleRequests_ShouldReadSequentially() { // Arrange var requests = """ {"jsonrpc":"2.0","method":"tools/list","id":"1"} {"jsonrpc":"2.0","method":"tools/call","id":"2"} """; var inputStream = new MemoryStream(Encoding.UTF8.GetBytes(requests)); var outputStream = new MemoryStream(); var transport = new StdioTransport(inputStream, outputStream); // Act var request1 = await transport.ReadRequestAsync(); var request2 = await transport.ReadRequestAsync(); // Assert Assert.NotNull(request1); Assert.Equal("1", request1.Id); Assert.Equal("tools/list", request1.Method); Assert.NotNull(request2); Assert.Equal("2", request2.Id); Assert.Equal("tools/call", request2.Method); } [Fact] public void StdioTransport_Dispose_ShouldNotThrow() { // Arrange var inputStream = new MemoryStream(); var outputStream = new MemoryStream(); var transport = new StdioTransport(inputStream, outputStream); // Act & Assert - should not throw transport.Dispose(); } [Fact] public async Task StdioTransport_WriteResponseAsync_NullResponse_ShouldThrow() { // Arrange var inputStream = new MemoryStream(); var outputStream = new MemoryStream(); var transport = new StdioTransport(inputStream, outputStream); // Act & Assert await Assert.ThrowsAsync(async () => await transport.WriteResponseAsync(null!) ); } }