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>
304 lines
7.5 KiB
C#
304 lines
7.5 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 CircuitBreaker following TDD approach.
|
|
/// Tests circuit breaker states: Closed, Open, HalfOpen.
|
|
/// </summary>
|
|
public class CircuitBreakerTests
|
|
{
|
|
[Fact]
|
|
public async Task ExecuteAsync_InClosedState_AllowsExecution()
|
|
{
|
|
// Arrange
|
|
var breaker = new CircuitBreaker
|
|
{
|
|
FailureThreshold = 3
|
|
};
|
|
var executed = false;
|
|
|
|
// Act
|
|
await breaker.ExecuteAsync(async () =>
|
|
{
|
|
executed = true;
|
|
await Task.CompletedTask;
|
|
});
|
|
|
|
// Assert
|
|
Assert.True(executed);
|
|
Assert.Equal(CircuitBreakerState.Closed, breaker.State);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ExecuteAsync_AfterMultipleFailures_OpensCircuit()
|
|
{
|
|
// Arrange
|
|
var breaker = new CircuitBreaker
|
|
{
|
|
FailureThreshold = 3,
|
|
OpenTimeout = TimeSpan.FromSeconds(1)
|
|
};
|
|
|
|
// Act - trigger 3 failures
|
|
for (int i = 0; i < 3; i++)
|
|
{
|
|
try
|
|
{
|
|
await breaker.ExecuteAsync<object>(async () =>
|
|
{
|
|
await Task.CompletedTask;
|
|
throw new Exception("Simulated failure");
|
|
});
|
|
}
|
|
catch
|
|
{
|
|
// Expected
|
|
}
|
|
}
|
|
|
|
// Assert
|
|
Assert.Equal(CircuitBreakerState.Open, breaker.State);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ExecuteAsync_WhenOpen_ThrowsCircuitBreakerOpenException()
|
|
{
|
|
// Arrange
|
|
var breaker = new CircuitBreaker
|
|
{
|
|
FailureThreshold = 1,
|
|
OpenTimeout = TimeSpan.FromSeconds(10)
|
|
};
|
|
|
|
// Trip the breaker
|
|
try
|
|
{
|
|
await breaker.ExecuteAsync<object>(async () =>
|
|
{
|
|
await Task.CompletedTask;
|
|
throw new Exception("Failure");
|
|
});
|
|
}
|
|
catch
|
|
{
|
|
// Expected
|
|
}
|
|
|
|
// Act & Assert - breaker should be open
|
|
await Assert.ThrowsAsync<CircuitBreakerOpenException>(() =>
|
|
breaker.ExecuteAsync(async () => await Task.CompletedTask));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ExecuteAsync_AfterTimeout_TransitionsToHalfOpen()
|
|
{
|
|
// Arrange
|
|
var breaker = new CircuitBreaker
|
|
{
|
|
FailureThreshold = 1,
|
|
OpenTimeout = TimeSpan.FromMilliseconds(100),
|
|
SuccessThreshold = 1 // Close after 1 success
|
|
};
|
|
|
|
// Trip the breaker
|
|
try
|
|
{
|
|
await breaker.ExecuteAsync<object>(async () =>
|
|
{
|
|
await Task.CompletedTask;
|
|
throw new Exception("Failure");
|
|
});
|
|
}
|
|
catch
|
|
{
|
|
// Expected
|
|
}
|
|
|
|
Assert.Equal(CircuitBreakerState.Open, breaker.State);
|
|
|
|
// Act - wait for timeout
|
|
await Task.Delay(150);
|
|
|
|
// Try another execution (should transition to half-open)
|
|
var executed = false;
|
|
await breaker.ExecuteAsync(async () =>
|
|
{
|
|
executed = true;
|
|
await Task.CompletedTask;
|
|
});
|
|
|
|
// Assert
|
|
Assert.True(executed);
|
|
Assert.Equal(CircuitBreakerState.Closed, breaker.State); // Successful execution closes the circuit
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ExecuteAsync_InHalfOpen_SuccessfulCallClosesCircuit()
|
|
{
|
|
// Arrange
|
|
var breaker = new CircuitBreaker
|
|
{
|
|
FailureThreshold = 1,
|
|
OpenTimeout = TimeSpan.FromMilliseconds(100),
|
|
SuccessThreshold = 1
|
|
};
|
|
|
|
// Trip the breaker
|
|
try
|
|
{
|
|
await breaker.ExecuteAsync<object>(async () =>
|
|
{
|
|
await Task.CompletedTask;
|
|
throw new Exception("Failure");
|
|
});
|
|
}
|
|
catch
|
|
{
|
|
// Expected
|
|
}
|
|
|
|
await Task.Delay(150); // Wait for open timeout
|
|
|
|
// Act - successful call in half-open
|
|
await breaker.ExecuteAsync(async () => await Task.CompletedTask);
|
|
|
|
// Assert
|
|
Assert.Equal(CircuitBreakerState.Closed, breaker.State);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ExecuteAsync_InHalfOpen_FailureReopensCircuit()
|
|
{
|
|
// Arrange
|
|
var breaker = new CircuitBreaker
|
|
{
|
|
FailureThreshold = 1,
|
|
OpenTimeout = TimeSpan.FromMilliseconds(100)
|
|
};
|
|
|
|
// Trip the breaker
|
|
try
|
|
{
|
|
await breaker.ExecuteAsync<object>(async () =>
|
|
{
|
|
await Task.CompletedTask;
|
|
throw new Exception("Failure");
|
|
});
|
|
}
|
|
catch
|
|
{
|
|
// Expected
|
|
}
|
|
|
|
await Task.Delay(150); // Wait for open timeout (now in half-open)
|
|
|
|
// Act - fail in half-open
|
|
try
|
|
{
|
|
await breaker.ExecuteAsync<object>(async () =>
|
|
{
|
|
await Task.CompletedTask;
|
|
throw new Exception("Another failure");
|
|
});
|
|
}
|
|
catch
|
|
{
|
|
// Expected
|
|
}
|
|
|
|
// Assert
|
|
Assert.Equal(CircuitBreakerState.Open, breaker.State);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ExecuteAsync_WithReturnValue_ReturnsResult()
|
|
{
|
|
// Arrange
|
|
var breaker = new CircuitBreaker();
|
|
var expectedValue = 42;
|
|
|
|
// Act
|
|
var result = await breaker.ExecuteAsync(async () =>
|
|
{
|
|
await Task.CompletedTask;
|
|
return expectedValue;
|
|
});
|
|
|
|
// Assert
|
|
Assert.Equal(expectedValue, result);
|
|
}
|
|
|
|
[Fact]
|
|
public void Reset_ClosesCircuitAndClearsFailures()
|
|
{
|
|
// Arrange
|
|
var breaker = new CircuitBreaker { FailureThreshold = 1 };
|
|
|
|
try
|
|
{
|
|
breaker.ExecuteAsync<object>(async () =>
|
|
{
|
|
await Task.CompletedTask;
|
|
throw new Exception("Failure");
|
|
}).GetAwaiter().GetResult();
|
|
}
|
|
catch
|
|
{
|
|
// Expected
|
|
}
|
|
|
|
Assert.Equal(CircuitBreakerState.Open, breaker.State);
|
|
|
|
// Act
|
|
breaker.Reset();
|
|
|
|
// Assert
|
|
Assert.Equal(CircuitBreakerState.Closed, breaker.State);
|
|
}
|
|
|
|
[Fact]
|
|
public void Constructor_SetsDefaultValues()
|
|
{
|
|
// Act
|
|
var breaker = new CircuitBreaker();
|
|
|
|
// Assert
|
|
Assert.Equal(CircuitBreakerState.Closed, breaker.State);
|
|
Assert.Equal(5, breaker.FailureThreshold);
|
|
Assert.Equal(3, breaker.SuccessThreshold);
|
|
Assert.Equal(TimeSpan.FromSeconds(60), breaker.OpenTimeout);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ExecuteAsync_TracksConcurrentFailures()
|
|
{
|
|
// Arrange
|
|
var breaker = new CircuitBreaker { FailureThreshold = 5 };
|
|
var failureCount = 0;
|
|
|
|
// Act - cause 4 failures (not enough to trip)
|
|
for (int i = 0; i < 4; i++)
|
|
{
|
|
try
|
|
{
|
|
await breaker.ExecuteAsync<object>(async () =>
|
|
{
|
|
await Task.CompletedTask;
|
|
throw new Exception("Failure");
|
|
});
|
|
}
|
|
catch
|
|
{
|
|
failureCount++;
|
|
}
|
|
}
|
|
|
|
// Assert - should still be closed
|
|
Assert.Equal(CircuitBreakerState.Closed, breaker.State);
|
|
Assert.Equal(4, failureCount);
|
|
}
|
|
}
|