secert updated
This commit is contained in:
131
utils/ratelimit_test.go
Normal file
131
utils/ratelimit_test.go
Normal file
@@ -0,0 +1,131 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
/*
|
||||
Being rate limited by the model provider.
|
||||
|
||||
Groq's free tier is 8,000 tokens a minute, which is three or four Buddy
|
||||
questions — so this is ordinary use, not an edge case. Its message reads:
|
||||
|
||||
Rate limit reached for model `openai/gpt-oss-120b` in organization
|
||||
`org_01m38x8s72e759kn6g88ve2dhj` service tier `on_demand` on tokens per
|
||||
minute (TPM): Limit 8000, Used 7320, Requested 792. Please try again in 840ms.
|
||||
|
||||
That sentence was being returned to the console as Buddy's ANSWER. It names our
|
||||
billing account, it is about arithmetic the shopkeeper cannot influence, and the
|
||||
condition it describes is usually over in under a second.
|
||||
*/
|
||||
|
||||
func TestTheWaitIsReadOutOfTheProvidersProse(t *testing.T) {
|
||||
// The part most likely to change, because it is somebody else's wording.
|
||||
for _, tc := range []struct {
|
||||
message string
|
||||
want time.Duration
|
||||
}{
|
||||
{"Please try again in 840ms.", 840 * time.Millisecond},
|
||||
{"please try again in 1.5s", 1500 * time.Millisecond},
|
||||
{"Try again in 2s. Need more tokens?", 2 * time.Second},
|
||||
{"Rate limit reached … Please try again in 397.499999ms.", 397499999 * time.Nanosecond},
|
||||
} {
|
||||
got := waitFromMessage(tc.message)
|
||||
// Milliseconds is the resolution that matters; the fractional tail of
|
||||
// 397.499999ms is noise.
|
||||
if (got - tc.want).Abs() > time.Millisecond {
|
||||
t.Fatalf("%q read as %v, want %v", tc.message, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestProseWithNoWaitInItReadsAsUnknown(t *testing.T) {
|
||||
// Zero means "no idea", and the caller uses its own floor. Guessing a
|
||||
// number here would be inventing one.
|
||||
for _, message := range []string{
|
||||
"", "Rate limit reached.", "try again later", "try again in soon", "try again in 0ms",
|
||||
} {
|
||||
if got := waitFromMessage(message); got != 0 {
|
||||
t.Fatalf("%q produced a wait of %v", message, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTheRetryAfterHeaderWinsOverTheProse(t *testing.T) {
|
||||
// It is the standard, and a proxy can add it where the body has nothing.
|
||||
resp := &http.Response{Header: http.Header{}}
|
||||
resp.Header.Set("Retry-After", "2")
|
||||
|
||||
got := retryAfter(resp, []byte(`{"error":{"message":"try again in 840ms"}}`))
|
||||
if got != 2*time.Second {
|
||||
t.Fatalf("header ignored: got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnUnwaitableLimitIsNotWaitedFor(t *testing.T) {
|
||||
// A hard quota can suggest a minute. Holding a request that long is worse
|
||||
// than saying "busy" — the person watching the spinner decided it was
|
||||
// broken long before it returned.
|
||||
if waitBeforeRetry(context.Background(), time.Minute) {
|
||||
t.Fatal("agreed to hold the request for a minute")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNothingIsWaitedForOnceTheCallerHasGone(t *testing.T) {
|
||||
// A retry after the browser gave up is work nobody will see.
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
if waitBeforeRetry(ctx, 10*time.Millisecond) {
|
||||
t.Fatal("waited although the request was already abandoned")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAShortWaitIsHonoured(t *testing.T) {
|
||||
start := time.Now()
|
||||
if !waitBeforeRetry(context.Background(), 30*time.Millisecond) {
|
||||
t.Fatal("refused a 30ms wait")
|
||||
}
|
||||
if time.Since(start) < 25*time.Millisecond {
|
||||
t.Fatal("returned without waiting")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBeingBusyIsRecognisableWithoutReadingTheText(t *testing.T) {
|
||||
// The HTTP layer answers 429 on this, and the console tells the person to
|
||||
// try again. Matching on the provider's wording instead would break the
|
||||
// moment Groq rephrases it.
|
||||
err := error(&tooManyRequests{what: "assistant", after: time.Second})
|
||||
|
||||
if !errors.Is(err, ErrBusy) {
|
||||
t.Fatal("a rate-limited call is not recognisable as busy")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTheProvidersBillingDetailsAreNotInTheMessage(t *testing.T) {
|
||||
// The whole point. Whatever the provider said, this is what a shopkeeper
|
||||
// reads.
|
||||
err := error(&tooManyRequests{what: "assistant", after: 840 * time.Millisecond})
|
||||
|
||||
for _, leaked := range []string{"org_", "TPM", "8000", "tier", "billing"} {
|
||||
if contains(err.Error(), leaked) {
|
||||
t.Fatalf("%q reaches the console: %s", leaked, err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func contains(haystack, needle string) bool {
|
||||
return len(needle) > 0 && len(haystack) >= len(needle) &&
|
||||
func() bool {
|
||||
for i := 0; i+len(needle) <= len(haystack); i++ {
|
||||
if haystack[i:i+len(needle)] == needle {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}()
|
||||
}
|
||||
Reference in New Issue
Block a user