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 }() }