- Renamed all directories: OpenHarbor.MCP.* → Svrnty.MCP.* - Updated all namespaces in 179 C# files - Renamed 20 .csproj files and 3 .sln files - Updated 193 documentation references - Updated 33 references in main CODEX codebase - Updated Codex.sln with new paths - Build verified: 0 errors Preparing for extraction to standalone repositories. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
260 lines
7.7 KiB
C#
260 lines
7.7 KiB
C#
using Xunit;
|
|
using Moq;
|
|
using System;
|
|
using System.IO;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace Svrnty.MCP.Core.Tests;
|
|
|
|
/// <summary>
|
|
/// Unit tests for StdioTransport following TDD approach.
|
|
/// Tests JSON-RPC 2.0 protocol over stdin/stdout communication.
|
|
/// </summary>
|
|
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<ArgumentNullException>(() =>
|
|
new StdioTransport(null!, outputStream)
|
|
);
|
|
}
|
|
|
|
[Fact]
|
|
public void StdioTransport_ShouldThrowOnNullOutputStream()
|
|
{
|
|
// Arrange
|
|
var inputStream = new MemoryStream();
|
|
|
|
// Act & Assert
|
|
Assert.Throws<ArgumentNullException>(() =>
|
|
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<JsonException>(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<ArgumentNullException>(async () =>
|
|
await transport.WriteResponseAsync(null!)
|
|
);
|
|
}
|
|
}
|