svrnty-mcp-gateway/tests/Svrnty.MCP.Gateway.Infrastructure.Tests/Health/CircuitBreakerTests.cs
Svrnty a4a1dd2e38 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

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);
}
}