Files
team-api/relay/scheduler/affinity_test.go
T
2026-05-10 12:21:27 +08:00

172 lines
4.2 KiB
Go

package scheduler
import (
"testing"
"time"
"github.com/qianfree/team-api/relay/constant"
)
func TestAffinityStore_SetGet(t *testing.T) {
store := NewAffinityStore()
store.Set(1, 1, "gpt-4o", 100)
ch, ok := store.Get(1, 1, "gpt-4o")
if !ok {
t.Fatal("expected affinity to be found")
}
if ch != 100 {
t.Errorf("expected channel 100, got %d", ch)
}
}
func TestAffinityStore_NotFound(t *testing.T) {
store := NewAffinityStore()
_, ok := store.Get(1, 999, "nonexistent")
if ok {
t.Error("expected affinity not to be found")
}
}
func TestAffinityStore_Expiry(t *testing.T) {
store := NewAffinityStore()
// Manually set an expired entry
store.mu.Lock()
store.entries[affinityKey{TenantID: 1, UserID: 1, ModelName: "test"}] = &affinityEntry{
ChannelID: 100,
ExpiresAt: time.Now().Add(-1 * time.Second),
}
store.mu.Unlock()
_, ok := store.Get(1, 1, "test")
if ok {
t.Error("expected expired affinity not to be found")
}
}
func TestAffinityStore_Delete(t *testing.T) {
store := NewAffinityStore()
store.Set(1, 1, "gpt-4o", 100)
store.Delete(1, 1, "gpt-4o")
_, ok := store.Get(1, 1, "gpt-4o")
if ok {
t.Error("expected affinity to be deleted")
}
}
func TestAffinityStore_DeleteByChannel(t *testing.T) {
store := NewAffinityStore()
store.Set(1, 1, "gpt-4o", 100)
store.Set(1, 2, "gpt-4o", 100) // same model, different user
store.Set(1, 1, "claude-3", 200)
store.DeleteByChannel(100)
_, ok1 := store.Get(1, 1, "gpt-4o")
_, ok2 := store.Get(1, 2, "gpt-4o")
_, ok3 := store.Get(1, 1, "claude-3")
if ok1 || ok2 {
t.Error("expected channel 100 affinities to be deleted")
}
if !ok3 {
t.Error("expected channel 200 affinity to remain")
}
}
func TestAffinityStore_HitCount(t *testing.T) {
store := NewAffinityStore()
store.Set(1, 1, "gpt-4o", 100)
store.Set(1, 1, "gpt-4o", 100) // same key, should increment
store.mu.RLock()
entry := store.entries[affinityKey{TenantID: 1, UserID: 1, ModelName: "gpt-4o"}]
store.mu.RUnlock()
if entry.HitCount != 2 {
t.Errorf("expected hit count 2, got %d", entry.HitCount)
}
}
func TestAffinityStore_TenantIsolation(t *testing.T) {
store := NewAffinityStore()
// Tenant 1, User 1 → channel 100
store.Set(1, 1, "gpt-4o", 100)
// Tenant 2, User 1 → channel 200 (same userID, different tenant)
store.Set(2, 1, "gpt-4o", 200)
ch1, ok1 := store.Get(1, 1, "gpt-4o")
ch2, ok2 := store.Get(2, 1, "gpt-4o")
if !ok1 || ch1 != 100 {
t.Errorf("tenant 1 expected channel 100, got %d, ok=%v", ch1, ok1)
}
if !ok2 || ch2 != 200 {
t.Errorf("tenant 2 expected channel 200, got %d, ok=%v", ch2, ok2)
}
// Deleting tenant 1's affinity should not affect tenant 2
store.Delete(1, 1, "gpt-4o")
_, ok1 = store.Get(1, 1, "gpt-4o")
_, ok2 = store.Get(2, 1, "gpt-4o")
if ok1 {
t.Error("expected tenant 1 affinity to be deleted")
}
if !ok2 {
t.Error("expected tenant 2 affinity to remain")
}
}
func TestAffinityStore_CleanExpired(t *testing.T) {
store := NewAffinityStore()
store.Set(1, 1, "active", 100)
store.Set(1, 2, "active", 200)
// Manually add an expired entry
store.mu.Lock()
store.entries[affinityKey{TenantID: 1, UserID: 3, ModelName: "expired"}] = &affinityEntry{
ChannelID: 300,
ExpiresAt: time.Now().Add(-1 * time.Second),
}
store.mu.Unlock()
cleaned := store.CleanExpired()
if cleaned != 1 {
t.Errorf("expected 1 expired entry cleaned, got %d", cleaned)
}
if store.Size() != 2 {
t.Errorf("expected 2 remaining entries, got %d", store.Size())
}
}
func TestRetryStrategy_ShouldRetry(t *testing.T) {
tests := []struct {
name string
err error
retryCount int
maxRetries int
shouldRetry bool
}{
{"nil error", nil, 0, 3, false},
{"non-retryable", constant.NewRequestError("bad request", nil), 0, 3, false},
{"retryable first attempt", constant.NewUpstreamError(500, "server error", nil), 0, 3, true},
{"retryable at max", constant.NewUpstreamError(500, "server error", nil), 3, 3, false},
{"429 rate limit", constant.NewUpstreamError(429, "rate limit", nil), 1, 3, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := ShouldRetry(tt.err, tt.retryCount, tt.maxRetries)
if result != tt.shouldRetry {
t.Errorf("ShouldRetry() = %v, want %v", result, tt.shouldRetry)
}
})
}
}