svrnty-mcp-server/tests/Svrnty.MCP.Core.Tests/StdioTransportTests.cs
Svrnty 516e1479c6 docs: comprehensive AI coding assistant research and MCP-first implementation plan
Research conducted on modern AI coding assistants (Cursor, GitHub Copilot, Cline,
Aider, Windsurf, Replit Agent) to understand architecture patterns, context management,
code editing workflows, and tool use protocols.

Key Decision: Pivoted from building full CLI (40-50h) to validation-driven MCP-first
approach (10-15h). Build 5 core CODEX MCP tools that work with ANY coding assistant,
validate adoption over 2-4 weeks, then decide on full CLI if demand proven.

Files:
- research/ai-systems/modern-coding-assistants-architecture.md (comprehensive research)
- research/ai-systems/codex-coding-assistant-implementation-plan.md (original CLI plan, preserved)
- research/ai-systems/codex-mcp-tools-implementation-plan.md (approved MCP-first plan)
- ideas/registry.json (updated with approved MCP tools proposal)

Architech Validation: APPROVED with pivot to MCP-first approach
Human Decision: Approved (pragmatic validation-driven development)

Next: Begin Phase 1 implementation (10-15 hours, 5 core MCP tools)

🤖 Generated with CODEX Research System

Co-Authored-By: The Archivist <archivist@codex.svrnty.io>
Co-Authored-By: The Architech <architech@codex.svrnty.io>
Co-Authored-By: Mathias Beaulieu-Duncan <mat@svrnty.io>
2025-10-22 21:00:34 -04:00

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