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>
323 lines
12 KiB
C#
323 lines
12 KiB
C#
using Xunit;
|
|
using Moq;
|
|
using OpenHarbor.MCP.Gateway.Infrastructure.Routing;
|
|
using OpenHarbor.MCP.Gateway.Infrastructure.Connection;
|
|
using OpenHarbor.MCP.Gateway.Core.Interfaces;
|
|
using OpenHarbor.MCP.Gateway.Core.Models;
|
|
|
|
namespace OpenHarbor.MCP.Gateway.Infrastructure.Tests.Routing;
|
|
|
|
/// <summary>
|
|
/// Unit tests for GatewayRouter following TDD approach.
|
|
/// Tests routing, server registration, and health tracking.
|
|
/// </summary>
|
|
public class GatewayRouterTests
|
|
{
|
|
[Fact]
|
|
public async Task RegisterServerAsync_AddsServerToPool()
|
|
{
|
|
// Arrange
|
|
var mockStrategy = new Mock<IRoutingStrategy>();
|
|
var mockPool = new Mock<IServerConnectionPool>();
|
|
var router = new GatewayRouter(mockStrategy.Object, mockPool.Object);
|
|
|
|
var serverConfig = new ServerConfig
|
|
{
|
|
Id = "server-1",
|
|
Name = "Test Server",
|
|
TransportType = "Http",
|
|
BaseUrl = "http://localhost:5000"
|
|
};
|
|
|
|
// Act
|
|
await router.RegisterServerAsync(serverConfig);
|
|
|
|
// Assert
|
|
var health = await router.GetServerHealthAsync();
|
|
Assert.Contains(health, s => s.ServerId == "server-1");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UnregisterServerAsync_RemovesServerFromPool()
|
|
{
|
|
// Arrange
|
|
var mockStrategy = new Mock<IRoutingStrategy>();
|
|
var mockPool = new Mock<IServerConnectionPool>();
|
|
var router = new GatewayRouter(mockStrategy.Object, mockPool.Object);
|
|
|
|
var serverConfig = new ServerConfig
|
|
{
|
|
Id = "server-1",
|
|
Name = "Test Server",
|
|
TransportType = "Http",
|
|
BaseUrl = "http://localhost:5000"
|
|
};
|
|
|
|
await router.RegisterServerAsync(serverConfig);
|
|
|
|
// Act
|
|
var result = await router.UnregisterServerAsync("server-1");
|
|
|
|
// Assert
|
|
Assert.True(result);
|
|
var health = await router.GetServerHealthAsync();
|
|
Assert.DoesNotContain(health, s => s.ServerId == "server-1");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UnregisterServerAsync_WithNonExistentServer_ReturnsFalse()
|
|
{
|
|
// Arrange
|
|
var mockStrategy = new Mock<IRoutingStrategy>();
|
|
var mockPool = new Mock<IServerConnectionPool>();
|
|
var router = new GatewayRouter(mockStrategy.Object, mockPool.Object);
|
|
|
|
// Act
|
|
var result = await router.UnregisterServerAsync("non-existent");
|
|
|
|
// Assert
|
|
Assert.False(result);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task RouteAsync_WithValidRequest_RoutesToSelectedServer()
|
|
{
|
|
// Arrange
|
|
var serverConfig = new ServerConfig
|
|
{
|
|
Id = "server-1",
|
|
Name = "Test Server",
|
|
TransportType = "Http",
|
|
BaseUrl = "http://localhost:5000"
|
|
};
|
|
|
|
var mockConnection = new Mock<IServerConnection>();
|
|
mockConnection.Setup(c => c.IsConnected).Returns(true);
|
|
mockConnection.Setup(c => c.ServerInfo).Returns(new ServerInfo { Id = "server-1", Name = "Test Server", IsHealthy = true });
|
|
mockConnection.Setup(c => c.SendRequestAsync(It.IsAny<GatewayRequest>(), It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(new GatewayResponse { Success = true });
|
|
|
|
var mockPool = new Mock<IServerConnectionPool>();
|
|
mockPool.Setup(p => p.GetConnectionAsync(It.IsAny<ServerConfig>(), It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(mockConnection.Object);
|
|
|
|
var mockStrategy = new Mock<IRoutingStrategy>();
|
|
mockStrategy.Setup(s => s.SelectServerAsync(It.IsAny<IEnumerable<ServerInfo>>(), It.IsAny<RoutingContext>(), It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(new ServerInfo { Id = "server-1", Name = "Test Server", IsHealthy = true });
|
|
|
|
var router = new GatewayRouter(mockStrategy.Object, mockPool.Object);
|
|
await router.RegisterServerAsync(serverConfig);
|
|
|
|
var request = new GatewayRequest { ToolName = "test_tool" };
|
|
|
|
// Act
|
|
var response = await router.RouteAsync(request);
|
|
|
|
// Assert
|
|
Assert.NotNull(response);
|
|
Assert.True(response.Success);
|
|
mockConnection.Verify(c => c.SendRequestAsync(request, It.IsAny<CancellationToken>()), Times.Once);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task RouteAsync_WithNoHealthyServers_ReturnsErrorResponse()
|
|
{
|
|
// Arrange
|
|
var mockStrategy = new Mock<IRoutingStrategy>();
|
|
mockStrategy.Setup(s => s.SelectServerAsync(It.IsAny<IEnumerable<ServerInfo>>(), It.IsAny<RoutingContext>(), It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync((ServerInfo?)null);
|
|
|
|
var mockPool = new Mock<ServerConnectionPool>();
|
|
var router = new GatewayRouter(mockStrategy.Object, mockPool.Object);
|
|
|
|
var request = new GatewayRequest { ToolName = "test_tool" };
|
|
|
|
// Act
|
|
var response = await router.RouteAsync(request);
|
|
|
|
// Assert
|
|
Assert.NotNull(response);
|
|
Assert.False(response.Success);
|
|
Assert.Contains("No healthy servers", response.Error);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetServerHealthAsync_ReturnsAllRegisteredServers()
|
|
{
|
|
// Arrange
|
|
var mockStrategy = new Mock<IRoutingStrategy>();
|
|
var mockPool = new Mock<IServerConnectionPool>();
|
|
var router = new GatewayRouter(mockStrategy.Object, mockPool.Object);
|
|
|
|
var server1 = new ServerConfig { Id = "server-1", Name = "Server 1", TransportType = "Http", BaseUrl = "http://localhost:5000" };
|
|
var server2 = new ServerConfig { Id = "server-2", Name = "Server 2", TransportType = "Http", BaseUrl = "http://localhost:5001" };
|
|
|
|
await router.RegisterServerAsync(server1);
|
|
await router.RegisterServerAsync(server2);
|
|
|
|
// Act
|
|
var health = await router.GetServerHealthAsync();
|
|
|
|
// Assert
|
|
Assert.Equal(2, health.Count());
|
|
Assert.Contains(health, s => s.ServerId == "server-1");
|
|
Assert.Contains(health, s => s.ServerId == "server-2");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task RouteAsync_UsesRoutingStrategy()
|
|
{
|
|
// Arrange
|
|
var serverConfig = new ServerConfig
|
|
{
|
|
Id = "selected-server",
|
|
Name = "Selected Server",
|
|
TransportType = "Http",
|
|
BaseUrl = "http://localhost:5000"
|
|
};
|
|
|
|
var mockConnection = new Mock<IServerConnection>();
|
|
mockConnection.Setup(c => c.IsConnected).Returns(true);
|
|
mockConnection.Setup(c => c.ServerInfo).Returns(new ServerInfo { Id = "selected-server", IsHealthy = true });
|
|
mockConnection.Setup(c => c.SendRequestAsync(It.IsAny<GatewayRequest>(), It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(new GatewayResponse { Success = true });
|
|
|
|
var mockPool = new Mock<IServerConnectionPool>();
|
|
mockPool.Setup(p => p.GetConnectionAsync(It.Is<ServerConfig>(sc => sc.Id == "selected-server"), It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(mockConnection.Object);
|
|
|
|
var mockStrategy = new Mock<IRoutingStrategy>();
|
|
mockStrategy.Setup(s => s.SelectServerAsync(It.IsAny<IEnumerable<ServerInfo>>(), It.IsAny<RoutingContext>(), It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(new ServerInfo { Id = "selected-server", IsHealthy = true });
|
|
|
|
var router = new GatewayRouter(mockStrategy.Object, mockPool.Object);
|
|
await router.RegisterServerAsync(serverConfig);
|
|
|
|
var request = new GatewayRequest { ToolName = "test_tool", ClientId = "test-client" };
|
|
|
|
// Act
|
|
await router.RouteAsync(request);
|
|
|
|
// Assert - verify strategy was called
|
|
mockStrategy.Verify(s => s.SelectServerAsync(
|
|
It.IsAny<IEnumerable<ServerInfo>>(),
|
|
It.Is<RoutingContext>(rc => rc.ToolName == "test_tool" && rc.ClientId == "test-client"),
|
|
It.IsAny<CancellationToken>()), Times.Once);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task RouteAsync_SetsServerIdInResponse()
|
|
{
|
|
// Arrange
|
|
var serverConfig = new ServerConfig
|
|
{
|
|
Id = "server-1",
|
|
Name = "Test Server",
|
|
TransportType = "Http",
|
|
BaseUrl = "http://localhost:5000"
|
|
};
|
|
|
|
var mockConnection = new Mock<IServerConnection>();
|
|
mockConnection.Setup(c => c.IsConnected).Returns(true);
|
|
mockConnection.Setup(c => c.ServerInfo).Returns(new ServerInfo { Id = "server-1", IsHealthy = true });
|
|
mockConnection.Setup(c => c.SendRequestAsync(It.IsAny<GatewayRequest>(), It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(new GatewayResponse { Success = true });
|
|
|
|
var mockPool = new Mock<IServerConnectionPool>();
|
|
mockPool.Setup(p => p.GetConnectionAsync(It.IsAny<ServerConfig>(), It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(mockConnection.Object);
|
|
|
|
var mockStrategy = new Mock<IRoutingStrategy>();
|
|
mockStrategy.Setup(s => s.SelectServerAsync(It.IsAny<IEnumerable<ServerInfo>>(), It.IsAny<RoutingContext>(), It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(new ServerInfo { Id = "server-1", IsHealthy = true });
|
|
|
|
var router = new GatewayRouter(mockStrategy.Object, mockPool.Object);
|
|
await router.RegisterServerAsync(serverConfig);
|
|
|
|
var request = new GatewayRequest { ToolName = "test_tool" };
|
|
|
|
// Act
|
|
var response = await router.RouteAsync(request);
|
|
|
|
// Assert
|
|
Assert.Equal("server-1", response.ServerId);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task RouteAsync_TracksPassiveHealth_OnSuccess()
|
|
{
|
|
// Arrange
|
|
var serverConfig = new ServerConfig
|
|
{
|
|
Id = "server-1",
|
|
Name = "Test Server",
|
|
TransportType = "Http",
|
|
BaseUrl = "http://localhost:5000"
|
|
};
|
|
|
|
var mockConnection = new Mock<IServerConnection>();
|
|
mockConnection.Setup(c => c.IsConnected).Returns(true);
|
|
mockConnection.Setup(c => c.ServerInfo).Returns(new ServerInfo { Id = "server-1", IsHealthy = true, ResponseTime = TimeSpan.FromMilliseconds(50) });
|
|
mockConnection.Setup(c => c.SendRequestAsync(It.IsAny<GatewayRequest>(), It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(new GatewayResponse { Success = true });
|
|
|
|
var mockPool = new Mock<IServerConnectionPool>();
|
|
mockPool.Setup(p => p.GetConnectionAsync(It.IsAny<ServerConfig>(), It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(mockConnection.Object);
|
|
|
|
var mockStrategy = new Mock<IRoutingStrategy>();
|
|
mockStrategy.Setup(s => s.SelectServerAsync(It.IsAny<IEnumerable<ServerInfo>>(), It.IsAny<RoutingContext>(), It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(new ServerInfo { Id = "server-1", IsHealthy = true });
|
|
|
|
var router = new GatewayRouter(mockStrategy.Object, mockPool.Object);
|
|
await router.RegisterServerAsync(serverConfig);
|
|
|
|
var request = new GatewayRequest { ToolName = "test_tool" };
|
|
|
|
// Act
|
|
await router.RouteAsync(request);
|
|
|
|
// Assert - passive health should be tracked (implementation will verify this)
|
|
mockConnection.Verify(c => c.SendRequestAsync(request, It.IsAny<CancellationToken>()), Times.Once);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task RouteAsync_TracksPassiveHealth_OnFailure()
|
|
{
|
|
// Arrange
|
|
var serverConfig = new ServerConfig
|
|
{
|
|
Id = "server-1",
|
|
Name = "Test Server",
|
|
TransportType = "Http",
|
|
BaseUrl = "http://localhost:5000"
|
|
};
|
|
|
|
var mockConnection = new Mock<IServerConnection>();
|
|
mockConnection.Setup(c => c.IsConnected).Returns(true);
|
|
mockConnection.Setup(c => c.ServerInfo).Returns(new ServerInfo { Id = "server-1", IsHealthy = true });
|
|
mockConnection.Setup(c => c.SendRequestAsync(It.IsAny<GatewayRequest>(), It.IsAny<CancellationToken>()))
|
|
.ThrowsAsync(new Exception("Request failed"));
|
|
|
|
var mockPool = new Mock<IServerConnectionPool>();
|
|
mockPool.Setup(p => p.GetConnectionAsync(It.IsAny<ServerConfig>(), It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(mockConnection.Object);
|
|
|
|
var mockStrategy = new Mock<IRoutingStrategy>();
|
|
mockStrategy.Setup(s => s.SelectServerAsync(It.IsAny<IEnumerable<ServerInfo>>(), It.IsAny<RoutingContext>(), It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(new ServerInfo { Id = "server-1", IsHealthy = true });
|
|
|
|
var router = new GatewayRouter(mockStrategy.Object, mockPool.Object);
|
|
await router.RegisterServerAsync(serverConfig);
|
|
|
|
var request = new GatewayRequest { ToolName = "test_tool" };
|
|
|
|
// Act
|
|
var response = await router.RouteAsync(request);
|
|
|
|
// Assert
|
|
Assert.False(response.Success);
|
|
Assert.Contains("Request failed", response.Error);
|
|
}
|
|
}
|