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>
223 lines
6.8 KiB
C#
223 lines
6.8 KiB
C#
using Xunit;
|
|
using OpenHarbor.MCP.Gateway.Infrastructure.Health;
|
|
using OpenHarbor.MCP.Gateway.Core.Models;
|
|
|
|
namespace OpenHarbor.MCP.Gateway.Infrastructure.Tests.Health;
|
|
|
|
/// <summary>
|
|
/// Unit tests for PassiveHealthTracker following TDD approach.
|
|
/// Tests passive health tracking based on response times and errors.
|
|
/// </summary>
|
|
public class PassiveHealthTrackerTests
|
|
{
|
|
[Fact]
|
|
public void RecordSuccess_UpdatesHealthStatus()
|
|
{
|
|
// Arrange
|
|
var tracker = new PassiveHealthTracker();
|
|
var serverId = "server-1";
|
|
var responseTime = TimeSpan.FromMilliseconds(50);
|
|
|
|
// Act
|
|
tracker.RecordSuccess(serverId, responseTime);
|
|
|
|
// Assert
|
|
var health = tracker.GetServerHealth(serverId);
|
|
Assert.NotNull(health);
|
|
Assert.True(health.IsHealthy);
|
|
Assert.Equal(responseTime, health.ResponseTime);
|
|
}
|
|
|
|
[Fact]
|
|
public void RecordFailure_UpdatesHealthStatus()
|
|
{
|
|
// Arrange
|
|
var tracker = new PassiveHealthTracker
|
|
{
|
|
UnhealthyThreshold = 1 // Mark unhealthy after 1 failure for this test
|
|
};
|
|
var serverId = "server-2";
|
|
var errorMessage = "Connection timeout";
|
|
|
|
// Act
|
|
tracker.RecordFailure(serverId, errorMessage);
|
|
|
|
// Assert
|
|
var health = tracker.GetServerHealth(serverId);
|
|
Assert.NotNull(health);
|
|
Assert.False(health.IsHealthy);
|
|
Assert.Equal(errorMessage, health.ErrorMessage);
|
|
}
|
|
|
|
[Fact]
|
|
public void RecordSuccess_MultipleRequests_CalculatesAverageResponseTime()
|
|
{
|
|
// Arrange
|
|
var tracker = new PassiveHealthTracker();
|
|
var serverId = "server-3";
|
|
|
|
// Act - record 3 successful requests
|
|
tracker.RecordSuccess(serverId, TimeSpan.FromMilliseconds(100));
|
|
tracker.RecordSuccess(serverId, TimeSpan.FromMilliseconds(200));
|
|
tracker.RecordSuccess(serverId, TimeSpan.FromMilliseconds(300));
|
|
|
|
// Assert
|
|
var health = tracker.GetServerHealth(serverId);
|
|
Assert.NotNull(health);
|
|
Assert.True(health.IsHealthy);
|
|
// Average should be around 200ms
|
|
Assert.NotNull(health.ResponseTime);
|
|
Assert.InRange(health.ResponseTime.Value.TotalMilliseconds, 150, 250);
|
|
}
|
|
|
|
[Fact]
|
|
public void RecordFailure_MultipleFailures_MarksServerUnhealthy()
|
|
{
|
|
// Arrange
|
|
var tracker = new PassiveHealthTracker
|
|
{
|
|
UnhealthyThreshold = 3 // Mark unhealthy after 3 failures
|
|
};
|
|
var serverId = "server-4";
|
|
|
|
// Act - record 3 failures
|
|
tracker.RecordFailure(serverId, "Error 1");
|
|
tracker.RecordFailure(serverId, "Error 2");
|
|
tracker.RecordFailure(serverId, "Error 3");
|
|
|
|
// Assert
|
|
var health = tracker.GetServerHealth(serverId);
|
|
Assert.NotNull(health);
|
|
Assert.False(health.IsHealthy);
|
|
}
|
|
|
|
[Fact]
|
|
public void RecordSuccess_AfterFailures_RecoverToHealthy()
|
|
{
|
|
// Arrange
|
|
var tracker = new PassiveHealthTracker
|
|
{
|
|
UnhealthyThreshold = 2,
|
|
HealthyThreshold = 3 // Recover after 3 successes
|
|
};
|
|
var serverId = "server-5";
|
|
|
|
// Record failures to make unhealthy
|
|
tracker.RecordFailure(serverId, "Error 1");
|
|
tracker.RecordFailure(serverId, "Error 2");
|
|
|
|
// Act - record successes to recover
|
|
tracker.RecordSuccess(serverId, TimeSpan.FromMilliseconds(50));
|
|
tracker.RecordSuccess(serverId, TimeSpan.FromMilliseconds(50));
|
|
tracker.RecordSuccess(serverId, TimeSpan.FromMilliseconds(50));
|
|
|
|
// Assert
|
|
var health = tracker.GetServerHealth(serverId);
|
|
Assert.NotNull(health);
|
|
Assert.True(health.IsHealthy);
|
|
}
|
|
|
|
[Fact]
|
|
public void GetServerHealth_WithUnknownServer_ReturnsNull()
|
|
{
|
|
// Arrange
|
|
var tracker = new PassiveHealthTracker();
|
|
|
|
// Act
|
|
var health = tracker.GetServerHealth("unknown-server");
|
|
|
|
// Assert
|
|
Assert.Null(health);
|
|
}
|
|
|
|
[Fact]
|
|
public void GetAllServerHealth_ReturnsAllTrackedServers()
|
|
{
|
|
// Arrange
|
|
var tracker = new PassiveHealthTracker
|
|
{
|
|
UnhealthyThreshold = 1 // Mark unhealthy after 1 failure for this test
|
|
};
|
|
|
|
tracker.RecordSuccess("server-1", TimeSpan.FromMilliseconds(50));
|
|
tracker.RecordSuccess("server-2", TimeSpan.FromMilliseconds(100));
|
|
tracker.RecordFailure("server-3", "Connection failed");
|
|
|
|
// Act
|
|
var allHealth = tracker.GetAllServerHealth();
|
|
|
|
// Assert
|
|
Assert.Equal(3, allHealth.Count());
|
|
Assert.Contains(allHealth, h => h.ServerId == "server-1" && h.IsHealthy);
|
|
Assert.Contains(allHealth, h => h.ServerId == "server-2" && h.IsHealthy);
|
|
Assert.Contains(allHealth, h => h.ServerId == "server-3" && !h.IsHealthy);
|
|
}
|
|
|
|
[Fact]
|
|
public void RecordSuccess_WithSlowResponse_MarksAsUnhealthy()
|
|
{
|
|
// Arrange
|
|
var tracker = new PassiveHealthTracker
|
|
{
|
|
SlowResponseThreshold = TimeSpan.FromMilliseconds(100)
|
|
};
|
|
var serverId = "server-6";
|
|
|
|
// Act - record slow response
|
|
tracker.RecordSuccess(serverId, TimeSpan.FromMilliseconds(500));
|
|
|
|
// Assert - should still be marked as success, but noted as slow
|
|
var health = tracker.GetServerHealth(serverId);
|
|
Assert.NotNull(health);
|
|
Assert.True(health.IsHealthy); // Still healthy, just slow
|
|
Assert.Equal(TimeSpan.FromMilliseconds(500), health.ResponseTime);
|
|
}
|
|
|
|
[Fact]
|
|
public void Reset_ClearsAllHealthData()
|
|
{
|
|
// Arrange
|
|
var tracker = new PassiveHealthTracker();
|
|
tracker.RecordSuccess("server-1", TimeSpan.FromMilliseconds(50));
|
|
tracker.RecordFailure("server-2", "Error");
|
|
|
|
// Act
|
|
tracker.Reset();
|
|
|
|
// Assert
|
|
var allHealth = tracker.GetAllServerHealth();
|
|
Assert.Empty(allHealth);
|
|
}
|
|
|
|
[Fact]
|
|
public void RecordSuccess_UpdatesLastCheckTime()
|
|
{
|
|
// Arrange
|
|
var tracker = new PassiveHealthTracker();
|
|
var serverId = "server-7";
|
|
var beforeRecord = DateTime.UtcNow;
|
|
|
|
// Act
|
|
tracker.RecordSuccess(serverId, TimeSpan.FromMilliseconds(50));
|
|
var afterRecord = DateTime.UtcNow;
|
|
|
|
// Assert
|
|
var health = tracker.GetServerHealth(serverId);
|
|
Assert.NotNull(health);
|
|
Assert.True(health.LastCheck >= beforeRecord);
|
|
Assert.True(health.LastCheck <= afterRecord);
|
|
}
|
|
|
|
[Fact]
|
|
public void Constructor_SetsDefaultThresholds()
|
|
{
|
|
// Act
|
|
var tracker = new PassiveHealthTracker();
|
|
|
|
// Assert
|
|
Assert.Equal(5, tracker.UnhealthyThreshold);
|
|
Assert.Equal(3, tracker.HealthyThreshold);
|
|
Assert.Equal(TimeSpan.FromSeconds(5), tracker.SlowResponseThreshold);
|
|
}
|
|
}
|