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!)
);
}
}