summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-06-18 07:37:47 +0300
committerPaul Buetow <paul@buetow.org>2026-06-18 07:37:47 +0300
commit3a5a04e006687044d01740974d0d1db72fe8c406 (patch)
tree7fddb8e8291a4c82fc5ef634ab0d1fdfffc4ec5a
parent13be0e094685cab3433bde35e5106bfeea822141 (diff)
Add negative test for API circuit breaker trip/short-circuit
The apicircuit package already wraps all external API calls (OpenAI TTS, Gemini TTS, OpenAI image/DALL-E, and Gemini Nano Banana image generation) with sony/gobreaker circuit breakers. This adds the missing resilience regression test verifying the breaker opens after the configured number of consecutive failures and short-circuits subsequent calls with gobreaker.ErrOpenState without invoking the wrapped function. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
-rw-r--r--internal/apicircuit/apicircuit_test.go38
1 files changed, 38 insertions, 0 deletions
diff --git a/internal/apicircuit/apicircuit_test.go b/internal/apicircuit/apicircuit_test.go
index 5f84c14..b6251a7 100644
--- a/internal/apicircuit/apicircuit_test.go
+++ b/internal/apicircuit/apicircuit_test.go
@@ -4,6 +4,8 @@ import (
"context"
"errors"
"testing"
+
+ "github.com/sony/gobreaker"
)
func TestOpenAITTS_Success(t *testing.T) {
@@ -16,6 +18,42 @@ func TestOpenAITTS_Success(t *testing.T) {
}
}
+// TestBreakerOpensAndShortCircuits verifies the core resilience guarantee: after
+// breakerTripAfterConsecutiveFailures failed calls the breaker opens, and further
+// calls are short-circuited with gobreaker.ErrOpenState without invoking fn. This
+// is what prevents a degraded API from causing cascading load.
+func TestBreakerOpensAndShortCircuits(t *testing.T) {
+ t.Parallel()
+
+ // Use a dedicated breaker (not the package singletons) so this test stays
+ // isolated from the others while still exercising the shared trip policy.
+ cb := newBreaker("test-trip")
+ apiErr := errors.New("upstream failure")
+
+ // Drive enough consecutive failures to trip the breaker open.
+ for i := uint32(0); i < breakerTripAfterConsecutiveFailures; i++ {
+ _, err := runValue(cb, func() (string, error) {
+ return "", apiErr
+ })
+ if !errors.Is(err, apiErr) {
+ t.Fatalf("call %d: got %v, want upstream failure", i, err)
+ }
+ }
+
+ // The breaker must now be open and short-circuit without calling fn.
+ called := false
+ _, err := runValue(cb, func() (string, error) {
+ called = true
+ return "ok", nil
+ })
+ if !errors.Is(err, gobreaker.ErrOpenState) {
+ t.Fatalf("expected ErrOpenState once breaker is open, got %v", err)
+ }
+ if called {
+ t.Fatal("open breaker must short-circuit and not invoke fn")
+ }
+}
+
func TestIsSuccessful_ContextCanceled(t *testing.T) {
t.Parallel()
if !isSuccessful(context.Canceled) {