vibe-proxy/backend/sdk/cliproxy/auth/conductor_compact_cooldown_test.go
2026-08-24 00:10:41 +02:00

302 lines
11 KiB
Go

package auth
import (
"context"
"errors"
"net/http"
"testing"
"time"
"github.com/router-for-me/CLIProxyAPI/v7/internal/registry"
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor"
)
type compactTestStatusError struct {
code int
msg string
}
func (e compactTestStatusError) Error() string { return e.msg }
func (e compactTestStatusError) StatusCode() int { return e.code }
type compactTestExecutor struct {
calls int
compactErr error
normalErr error
responseBody []byte
}
func (e *compactTestExecutor) Identifier() string { return "compact-test-provider" }
func (e *compactTestExecutor) Execute(ctx context.Context, auth *Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (cliproxyexecutor.Response, error) {
e.calls++
if opts.Alt == "responses/compact" {
if e.compactErr != nil {
return cliproxyexecutor.Response{}, e.compactErr
}
} else {
if e.normalErr != nil {
return cliproxyexecutor.Response{}, e.normalErr
}
}
payload := e.responseBody
if len(payload) == 0 {
payload = []byte(`{"status":"ok"}`)
}
return cliproxyexecutor.Response{Payload: payload}, nil
}
func (e *compactTestExecutor) ExecuteStream(context.Context, *Auth, cliproxyexecutor.Request, cliproxyexecutor.Options) (*cliproxyexecutor.StreamResult, error) {
return nil, errors.New("stream not supported")
}
func (e *compactTestExecutor) Refresh(ctx context.Context, auth *Auth) (*Auth, error) {
return auth, nil
}
func (e *compactTestExecutor) CountTokens(context.Context, *Auth, cliproxyexecutor.Request, cliproxyexecutor.Options) (cliproxyexecutor.Response, error) {
return cliproxyexecutor.Response{}, errors.New("not supported")
}
func (e *compactTestExecutor) HttpRequest(context.Context, *Auth, *http.Request) (*http.Response, error) {
return nil, errors.New("not supported")
}
func TestManager_ResponsesCompact_TransientFailure_AvailabilityNeutral(t *testing.T) {
executor := &compactTestExecutor{
compactErr: compactTestStatusError{code: http.StatusInternalServerError, msg: "upstream compact 500"},
}
m := NewManager(nil, nil, nil)
m.RegisterExecutor(executor)
model := "gpt-5.6-sol"
auth1 := &Auth{ID: "auth1", Provider: executor.Identifier(), Status: StatusActive}
auth2 := &Auth{ID: "auth2", Provider: executor.Identifier(), Status: StatusActive}
if _, err := m.Register(context.Background(), auth1); err != nil {
t.Fatalf("Register auth1: %v", err)
}
if _, err := m.Register(context.Background(), auth2); err != nil {
t.Fatalf("Register auth2: %v", err)
}
registry.GetGlobalRegistry().RegisterClient(auth1.ID, auth1.Provider, []*registry.ModelInfo{{ID: model}})
registry.GetGlobalRegistry().RegisterClient(auth2.ID, auth2.Provider, []*registry.ModelInfo{{ID: model}})
t.Cleanup(func() {
registry.GetGlobalRegistry().UnregisterClient(auth1.ID)
registry.GetGlobalRegistry().UnregisterClient(auth2.ID)
})
req := cliproxyexecutor.Request{Model: model, Payload: []byte(`{"input":"hello"}`)}
opts := cliproxyexecutor.Options{Alt: "responses/compact"}
start := time.Now()
_, errExec := m.Execute(context.Background(), []string{executor.Identifier()}, req, opts)
elapsed := time.Since(start)
if errExec == nil {
t.Fatal("Execute expected error, got nil")
}
if elapsed > 2*time.Second {
t.Fatalf("Execute took %v, should not pause for cooldown wait", elapsed)
}
if executor.calls != 2 {
t.Fatalf("executor.calls = %d, want 2 (fallback across candidate auths)", executor.calls)
}
// Verify model states are not unavailable
for _, id := range []string{"auth1", "auth2"} {
a, ok := m.GetByID(id)
if !ok {
t.Fatalf("auth %s not found", id)
}
if state, exists := a.ModelStates[model]; exists && state != nil {
if state.Unavailable {
t.Fatalf("auth %s marked unavailable after compact failure", id)
}
if !state.NextRetryAfter.IsZero() && state.NextRetryAfter.After(time.Now()) {
t.Fatalf("auth %s has NextRetryAfter set in future: %v", id, state.NextRetryAfter)
}
}
}
// Normal request succeeds immediately
normalReq := cliproxyexecutor.Request{Model: model, Payload: []byte(`{"input":"hello"}`)}
normalOpts := cliproxyexecutor.Options{}
resp, errNormal := m.Execute(context.Background(), []string{executor.Identifier()}, normalReq, normalOpts)
if errNormal != nil {
t.Fatalf("normal Execute failed: %v", errNormal)
}
if string(resp.Payload) != `{"status":"ok"}` {
t.Fatalf("normal Execute payload = %s", string(resp.Payload))
}
}
func TestManager_ResponsesCompact_RequestFault_StopsFallback(t *testing.T) {
executor := &compactTestExecutor{
compactErr: compactTestStatusError{code: http.StatusNotFound, msg: "404 endpoint not found"},
}
m := NewManager(nil, nil, nil)
m.RegisterExecutor(executor)
model := "gpt-5.6-sol"
auth1 := &Auth{ID: "auth1", Provider: executor.Identifier(), Status: StatusActive}
auth2 := &Auth{ID: "auth2", Provider: executor.Identifier(), Status: StatusActive}
if _, err := m.Register(context.Background(), auth1); err != nil {
t.Fatalf("Register auth1: %v", err)
}
if _, err := m.Register(context.Background(), auth2); err != nil {
t.Fatalf("Register auth2: %v", err)
}
registry.GetGlobalRegistry().RegisterClient(auth1.ID, auth1.Provider, []*registry.ModelInfo{{ID: model}})
registry.GetGlobalRegistry().RegisterClient(auth2.ID, auth2.Provider, []*registry.ModelInfo{{ID: model}})
t.Cleanup(func() {
registry.GetGlobalRegistry().UnregisterClient(auth1.ID)
registry.GetGlobalRegistry().UnregisterClient(auth2.ID)
})
req := cliproxyexecutor.Request{Model: model, Payload: []byte(`{"input":"hello"}`)}
opts := cliproxyexecutor.Options{Alt: "responses/compact"}
_, errExec := m.Execute(context.Background(), []string{executor.Identifier()}, req, opts)
if errExec == nil {
t.Fatal("Execute expected error, got nil")
}
if executor.calls != 1 {
t.Fatalf("executor.calls = %d, want 1 (fallback stopped on request fault)", executor.calls)
}
// Verify model states are not unavailable
for _, id := range []string{"auth1", "auth2"} {
a, ok := m.GetByID(id)
if !ok {
t.Fatalf("auth %s not found", id)
}
if state, exists := a.ModelStates[model]; exists && state != nil {
if state.Unavailable {
t.Fatalf("auth %s marked unavailable after compact 404 fault", id)
}
}
}
}
func TestManager_ResponsesCompact_Unauthorized_CoolsCredential(t *testing.T) {
executor := &compactTestExecutor{
compactErr: compactTestStatusError{code: http.StatusUnauthorized, msg: "401 unauthorized"},
}
m := NewManager(nil, nil, nil)
m.RegisterExecutor(executor)
model := "gpt-5.6-sol"
auth1 := &Auth{ID: "auth1", Provider: executor.Identifier(), Status: StatusActive}
if _, err := m.Register(context.Background(), auth1); err != nil {
t.Fatalf("Register auth1: %v", err)
}
registry.GetGlobalRegistry().RegisterClient(auth1.ID, auth1.Provider, []*registry.ModelInfo{{ID: model}})
t.Cleanup(func() {
registry.GetGlobalRegistry().UnregisterClient(auth1.ID)
})
req := cliproxyexecutor.Request{Model: model, Payload: []byte(`{"input":"hello"}`)}
opts := cliproxyexecutor.Options{Alt: "responses/compact"}
_, errExec := m.Execute(context.Background(), []string{executor.Identifier()}, req, opts)
if errExec == nil {
t.Fatal("Execute expected error, got nil")
}
a, ok := m.GetByID("auth1")
if !ok {
t.Fatal("auth1 not found")
}
state, exists := a.ModelStates[model]
if !exists || state == nil {
t.Fatal("auth1 model state should be recorded for 401 unauthorized")
}
if !state.Unavailable {
t.Fatal("auth1 model state should be unavailable after 401 unauthorized")
}
if state.NextRetryAfter.IsZero() || !state.NextRetryAfter.After(time.Now()) {
t.Fatalf("auth1 NextRetryAfter not set in future for 401 unauthorized: %v", state.NextRetryAfter)
}
}
func TestManager_ResponsesCompact_Forbidden_CoolsCredential(t *testing.T) {
executor := &compactTestExecutor{
compactErr: compactTestStatusError{code: http.StatusForbidden, msg: "403 forbidden"},
}
m := NewManager(nil, nil, nil)
m.RegisterExecutor(executor)
model := "gpt-5.6-sol"
auth1 := &Auth{ID: "auth1", Provider: executor.Identifier(), Status: StatusActive}
if _, err := m.Register(context.Background(), auth1); err != nil {
t.Fatalf("Register auth1: %v", err)
}
registry.GetGlobalRegistry().RegisterClient(auth1.ID, auth1.Provider, []*registry.ModelInfo{{ID: model}})
t.Cleanup(func() {
registry.GetGlobalRegistry().UnregisterClient(auth1.ID)
})
req := cliproxyexecutor.Request{Model: model, Payload: []byte(`{"input":"hello"}`)}
opts := cliproxyexecutor.Options{Alt: "responses/compact"}
_, errExec := m.Execute(context.Background(), []string{executor.Identifier()}, req, opts)
if errExec == nil {
t.Fatal("Execute expected error, got nil")
}
a, ok := m.GetByID("auth1")
if !ok {
t.Fatal("auth1 not found")
}
state, exists := a.ModelStates[model]
if !exists || state == nil {
t.Fatal("auth1 model state should be recorded for 403 forbidden")
}
if !state.Unavailable {
t.Fatal("auth1 model state should be unavailable after 403 forbidden")
}
if state.NextRetryAfter.IsZero() || !state.NextRetryAfter.After(time.Now()) {
t.Fatalf("auth1 NextRetryAfter not set in future for 403 forbidden: %v", state.NextRetryAfter)
}
}
func TestManager_ResponsesCompact_Quota429_CoolsCredential(t *testing.T) {
executor := &compactTestExecutor{
compactErr: compactTestStatusError{code: http.StatusTooManyRequests, msg: `{"error":{"type":"usage_limit_reached","message":"quota exceeded"}}`},
}
m := NewManager(nil, nil, nil)
m.RegisterExecutor(executor)
model := "gpt-5.6-sol"
auth1 := &Auth{ID: "auth1", Provider: executor.Identifier(), Status: StatusActive}
if _, err := m.Register(context.Background(), auth1); err != nil {
t.Fatalf("Register auth1: %v", err)
}
registry.GetGlobalRegistry().RegisterClient(auth1.ID, auth1.Provider, []*registry.ModelInfo{{ID: model}})
t.Cleanup(func() {
registry.GetGlobalRegistry().UnregisterClient(auth1.ID)
})
req := cliproxyexecutor.Request{Model: model, Payload: []byte(`{"input":"hello"}`)}
opts := cliproxyexecutor.Options{Alt: "responses/compact"}
_, errExec := m.Execute(context.Background(), []string{executor.Identifier()}, req, opts)
if errExec == nil {
t.Fatal("Execute expected error, got nil")
}
a, ok := m.GetByID("auth1")
if !ok {
t.Fatal("auth1 not found")
}
state, exists := a.ModelStates[model]
if !exists || state == nil {
t.Fatal("auth1 model state should be recorded for 429 quota")
}
if !state.Quota.Exceeded {
t.Fatal("auth1 quota should be marked exceeded after 429 quota")
}
if state.NextRetryAfter.IsZero() || !state.NextRetryAfter.After(time.Now()) {
t.Fatalf("auth1 NextRetryAfter not set in future for 429 quota: %v", state.NextRetryAfter)
}
}