Pace provider traffic and identify genuine foreground bank requests

This commit is contained in:
Lars Nolden
2026-09-10 17:47:44 +02:00
parent ba3ea6ae5a
commit 4324660888
13 changed files with 817 additions and 32 deletions
@@ -12,8 +12,63 @@ import (
"sync/atomic"
"testing"
"time"
"finance-duck/internal/ratelimit"
)
func TestProductionRatePolicyAcrossModelSnapshots(t *testing.T) {
t.Run("successful classifications stay paced", func(t *testing.T) {
f, d := fixture()
arrivals := make(chan time.Time, 2)
c := mockClient(t, func(w http.ResponseWriter, r *http.Request) {
arrivals <- time.Now()
reply(w, validAnswer)
})
// Remove only the fixture's fast policy, exercising real lazy production
// initialization and the same shared controller used by previews.
c.rate.Store(nil)
snapshot := c.WithModel("test/preview-model")
ctx, cancel := context.WithTimeout(context.Background(), 6*time.Second)
defer cancel()
for _, client := range []*Client{c, snapshot} {
p, err := client.Classify(ctx, f, d, true)
if err != nil || p.Enrichment.Classification.Source != "openrouter" {
t.Fatalf("production-paced classification failed: %+v, %v", p, err)
}
}
first, second := <-arrivals, <-arrivals
if gap := second.Sub(first); gap < 3*time.Second-25*time.Millisecond {
t.Fatalf("model snapshots burst after success: %v", gap)
}
})
t.Run("first failure retains conservative retry deadline", func(t *testing.T) {
f, d := fixture()
var calls atomic.Int32
c := mockClient(t, func(w http.ResponseWriter, r *http.Request) {
calls.Add(1)
w.WriteHeader(http.StatusTooManyRequests)
})
c.rate.Store(nil)
snapshot := c.WithModel("test/preview-model")
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
_, err := c.Classify(ctx, f, d, true)
var limit *ratelimit.RateLimitError
if !errors.Is(err, context.DeadlineExceeded) || !errors.As(err, &limit) {
t.Fatalf("production retry wait lost cancellation or quota identity: %v", err)
}
if delay := time.Until(limit.RetryAt()); delay < 14*time.Second {
t.Fatalf("production retry used a short per-record fallback: %v", delay)
}
probeCtx, probeCancel := context.WithTimeout(context.Background(), time.Second)
defer probeCancel()
_, err = snapshot.Classify(probeCtx, f, d, true)
if !errors.As(err, &limit) || errors.Is(err, context.DeadlineExceeded) || calls.Load() != 1 {
t.Fatalf("preview discarded production cooldown: %v, calls=%d", err, calls.Load())
}
})
}
func TestRateLimitRetryPreservesPrivateRequest(t *testing.T) {
f, d := fixture()
f.Counterparty = "Alice Privateperson"