diff options
Diffstat (limited to 'internal/apicircuit/apicircuit_test.go')
| -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) { |
