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