aibridge/interceptionerror/error_test.go
Rama main · réplica de lectura del repositorio original.
package interceptionerror_test
import (
"context"
"strings"
"testing"
"unicode/utf8"
"github.com/stretchr/testify/assert"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/aibridge/circuitbreaker"
"github.com/coder/coder/v2/aibridge/interceptionerror"
"github.com/coder/coder/v2/aibridge/keypool"
"github.com/coder/coder/v2/aibridge/recorder"
)
// stubCategorizer is a test ErrorCategorizer standing in for a provider.
type stubCategorizer struct {
result *recorder.ErrorType
}
func (s stubCategorizer) CategorizeError(error) *recorder.ErrorType {
return s.result
}
func ptr(t recorder.ErrorType) *recorder.ErrorType { return &t }
func TestCategorize(t *testing.T) {
t.Parallel()
cases := []struct {
name string
cat stubCategorizer
err error
wantType recorder.ErrorType
wantMsg string
}{
{
name: "nil success",
err: nil,
wantType: "",
wantMsg: "",
},
{
name: "circuit open maps to server error",
err: circuitbreaker.ErrCircuitOpen,
wantType: recorder.ErrorTypeServerError,
wantMsg: circuitbreaker.ErrCircuitOpen.Error(),
},
{
name: "context deadline is timeout",
err: context.DeadlineExceeded,
wantType: recorder.ErrorTypeTimeout,
wantMsg: context.DeadlineExceeded.Error(),
},
{
name: "keypool permanent is unauthorized",
err: &keypool.Error{Kind: keypool.ErrorKindPermanent},
wantType: recorder.ErrorTypeUnauthorized,
wantMsg: (&keypool.Error{Kind: keypool.ErrorKindPermanent}).Error(),
},
{
name: "keypool unauthorized is unauthorized",
err: &keypool.Error{Kind: keypool.ErrorKindUnauthorized},
wantType: recorder.ErrorTypeUnauthorized,
wantMsg: (&keypool.Error{Kind: keypool.ErrorKindUnauthorized}).Error(),
},
{
name: "keypool rate limited is rate limited",
err: &keypool.Error{Kind: keypool.ErrorKindRateLimited},
wantType: recorder.ErrorTypeRateLimited,
wantMsg: (&keypool.Error{Kind: keypool.ErrorKindRateLimited}).Error(),
},
{
name: "keypool unrecognized kind is unknown",
err: &keypool.Error{Kind: keypool.ErrorKind(-1)},
wantType: recorder.ErrorTypeUnknown,
wantMsg: (&keypool.Error{Kind: keypool.ErrorKind(-1)}).Error(),
},
{
name: "context canceled is unknown",
err: context.Canceled,
wantType: recorder.ErrorTypeUnknown,
wantMsg: context.Canceled.Error(),
},
{
name: "wrapped keypool error is unwrapped",
err: xerrors.Errorf("key pool exhausted: %w", &keypool.Error{Kind: keypool.ErrorKindPermanent}),
wantType: recorder.ErrorTypeUnauthorized,
wantMsg: "key pool exhausted: all configured keys are permanently unavailable",
},
{
name: "delegated to provider",
cat: stubCategorizer{result: ptr(recorder.ErrorTypeOverloaded)},
err: xerrors.New("provider error"),
wantType: recorder.ErrorTypeOverloaded,
wantMsg: "provider error",
},
{
name: "provider does not recognize the error",
err: xerrors.New("mystery"),
wantType: recorder.ErrorTypeUnknown,
wantMsg: "mystery",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
gotType, gotMsg := interceptionerror.Categorize(tc.cat, tc.err)
assert.Equal(t, tc.wantType, gotType)
assert.Equal(t, tc.wantMsg, gotMsg)
})
}
}
func TestCategorizeTruncatesMessage(t *testing.T) {
t.Parallel()
const maxRecordedErrorMessageBytes = 1024
// ASCII: truncated exactly at the byte cap.
ascii := strings.Repeat("a", maxRecordedErrorMessageBytes*2)
_, gotMsg := interceptionerror.Categorize(stubCategorizer{}, xerrors.New(ascii))
assert.Len(t, gotMsg, maxRecordedErrorMessageBytes)
// Multi-byte: the '€' rune (3 bytes) split at the cap is dropped, leaving
// valid UTF-8 just below the cap rather than an invalid trailing fragment.
multibyte := strings.Repeat("€", maxRecordedErrorMessageBytes)
_, gotMsg = interceptionerror.Categorize(stubCategorizer{}, xerrors.New(multibyte))
assert.True(t, utf8.ValidString(gotMsg), "truncated message must stay valid UTF-8")
assert.Less(t, len(gotMsg), maxRecordedErrorMessageBytes)
assert.Positive(t, len(gotMsg))
}