veylant/internal/circuitbreaker/breaker_test.go
2026-02-23 13:35:04 +01:00

106 lines
2.5 KiB
Go

package circuitbreaker_test
import (
"sync"
"testing"
"time"
"github.com/veylant/ia-gateway/internal/circuitbreaker"
)
func TestAllowWhenClosed(t *testing.T) {
b := circuitbreaker.New(5, 60*time.Second)
if !b.Allow("openai") {
t.Fatal("expected Allow=true for a fresh Closed circuit")
}
}
func TestRejectWhenOpen(t *testing.T) {
b := circuitbreaker.New(3, 60*time.Second)
// Trip the circuit.
for i := 0; i < 3; i++ {
b.Failure("openai")
}
if b.Allow("openai") {
t.Fatal("expected Allow=false when circuit is Open")
}
s := b.Status("openai")
if s.State != "open" {
t.Fatalf("expected state=open, got %s", s.State)
}
}
func TestOpenAfterThreshold(t *testing.T) {
b := circuitbreaker.New(5, 60*time.Second)
// 4 failures: still closed.
for i := 0; i < 4; i++ {
b.Failure("anthropic")
}
if !b.Allow("anthropic") {
t.Fatal("expected Allow=true before threshold reached")
}
// 5th failure: opens.
b.Failure("anthropic")
if b.Allow("anthropic") {
t.Fatal("expected Allow=false after threshold reached")
}
}
func TestHalfOpenAfterTTL(t *testing.T) {
b := circuitbreaker.New(3, 10*time.Millisecond)
// Trip the circuit.
for i := 0; i < 3; i++ {
b.Failure("mistral")
}
if b.Allow("mistral") {
t.Fatal("circuit should be Open immediately after threshold")
}
// Wait for TTL.
time.Sleep(20 * time.Millisecond)
// First Allow should return true (HalfOpen probe).
if !b.Allow("mistral") {
t.Fatal("expected Allow=true in HalfOpen state after TTL")
}
if b.Status("mistral").State != "half_open" {
t.Fatalf("expected state=half_open, got %s", b.Status("mistral").State)
}
}
func TestCloseAfterSuccess(t *testing.T) {
b := circuitbreaker.New(3, 5*time.Millisecond)
for i := 0; i < 3; i++ {
b.Failure("ollama")
}
time.Sleep(10 * time.Millisecond)
b.Allow("ollama") // enter HalfOpen
b.Success("ollama")
if b.Status("ollama").State != "closed" {
t.Fatalf("expected state=closed after success, got %s", b.Status("ollama").State)
}
if b.Status("ollama").Failures != 0 {
t.Fatal("expected failures=0 after success")
}
}
func TestConcurrentSafe(t *testing.T) {
b := circuitbreaker.New(100, 60*time.Second)
var wg sync.WaitGroup
for i := 0; i < 200; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
if i%3 == 0 {
b.Failure("azure")
} else if i%3 == 1 {
b.Success("azure")
} else {
b.Allow("azure")
}
}(i)
}
wg.Wait()
// Just check no panic and Status is reachable.
_ = b.Status("azure")
_ = b.Statuses()
}