diff options
| author | Paul Buetow <paul@buetow.org> | 2026-06-18 07:37:47 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-06-18 07:37:47 +0300 |
| commit | 3a5a04e006687044d01740974d0d1db72fe8c406 (patch) | |
| tree | 7fddb8e8291a4c82fc5ef634ab0d1fdfffc4ec5a | |
| parent | 13be0e094685cab3433bde35e5106bfeea822141 (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.go | 38 |
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) { |
