Add projects
This commit is contained in:
parent
2d3a9ad623
commit
8b607dd700
1802 changed files with 503346 additions and 2 deletions
556
backend/sdk/cliproxy/auth/home_concurrency_test.go
Normal file
556
backend/sdk/cliproxy/auth/home_concurrency_test.go
Normal file
|
|
@ -0,0 +1,556 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
internalconfig "github.com/router-for-me/CLIProxyAPI/v7/internal/config"
|
||||
"github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executionregistry"
|
||||
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor"
|
||||
)
|
||||
|
||||
type fixtureHomeDispatcher struct {
|
||||
payload []byte
|
||||
payloads [][]byte
|
||||
calls int
|
||||
closedForAmbiguity bool
|
||||
onAbort func()
|
||||
}
|
||||
|
||||
func (d *fixtureHomeDispatcher) HeartbeatOK() bool { return true }
|
||||
|
||||
func (d *fixtureHomeDispatcher) RPopAuth(context.Context, string, string, http.Header, int) ([]byte, error) {
|
||||
if len(d.payloads) == 0 {
|
||||
return d.payload, nil
|
||||
}
|
||||
if d.calls >= len(d.payloads) {
|
||||
return nil, errors.New("unexpected Home dispatch")
|
||||
}
|
||||
payload := d.payloads[d.calls]
|
||||
d.calls++
|
||||
return payload, nil
|
||||
}
|
||||
|
||||
func (d *fixtureHomeDispatcher) AbortAmbiguousDispatch() {
|
||||
d.closedForAmbiguity = true
|
||||
if d.onAbort != nil {
|
||||
d.onAbort()
|
||||
}
|
||||
}
|
||||
|
||||
func newHomeSelectionTestManager(t *testing.T, dispatcher homeAuthDispatcher) *Manager {
|
||||
t.Helper()
|
||||
manager := NewManager(nil, nil, nil)
|
||||
manager.PublishHomeDispatch(dispatcher, executionregistry.New(), 1)
|
||||
return manager
|
||||
}
|
||||
|
||||
type busyHomeRetryDispatcher struct {
|
||||
calls atomic.Int32
|
||||
}
|
||||
|
||||
func (*busyHomeRetryDispatcher) HeartbeatOK() bool { return true }
|
||||
|
||||
func (d *busyHomeRetryDispatcher) RPopAuth(context.Context, string, string, http.Header, int) ([]byte, error) {
|
||||
d.calls.Add(1)
|
||||
return []byte(`{"error":{"type":"credential_concurrency_exceeded","message":"busy","retryable":true,"retry_after_ms":20000}}`), nil
|
||||
}
|
||||
|
||||
func (*busyHomeRetryDispatcher) AbortAmbiguousDispatch() {}
|
||||
|
||||
func TestHomeBusySkipsNormalAndStreamOuterRetries(t *testing.T) {
|
||||
for _, stream := range []bool{false, true} {
|
||||
t.Run(map[bool]string{false: "normal", true: "stream"}[stream], func(t *testing.T) {
|
||||
dispatcher := &busyHomeRetryDispatcher{}
|
||||
manager := NewManager(nil, nil, nil)
|
||||
manager.SetConfig(&internalconfig.Config{Home: internalconfig.HomeConfig{Enabled: true}})
|
||||
manager.SetRetryConfig(1, 30*time.Second, 0)
|
||||
manager.PublishHomeDispatch(dispatcher, executionregistry.New(), 1)
|
||||
if _, errRegister := manager.Register(context.Background(), &Auth{ID: "retry-auth", Provider: "home-busy"}); errRegister != nil {
|
||||
t.Fatalf("register retry auth: %v", errRegister)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
result := make(chan error, 1)
|
||||
started := time.Now()
|
||||
go func() {
|
||||
if stream {
|
||||
_, errExecute := manager.ExecuteStream(ctx, []string{"home-busy"}, cliproxyexecutor.Request{Model: "gpt"}, cliproxyexecutor.Options{Stream: true})
|
||||
result <- errExecute
|
||||
return
|
||||
}
|
||||
_, errExecute := manager.Execute(ctx, []string{"home-busy"}, cliproxyexecutor.Request{Model: "gpt"}, cliproxyexecutor.Options{})
|
||||
result <- errExecute
|
||||
}()
|
||||
|
||||
select {
|
||||
case errExecute := <-result:
|
||||
var busy *HomeConcurrencyBusyError
|
||||
if !errors.As(errExecute, &busy) {
|
||||
t.Fatalf("execution error = %v, want HomeConcurrencyBusyError", errExecute)
|
||||
}
|
||||
case <-time.After(250 * time.Millisecond):
|
||||
t.Fatal("Home busy waited for its retry hint")
|
||||
}
|
||||
if elapsed := time.Since(started); elapsed >= 250*time.Millisecond {
|
||||
t.Fatalf("Home busy returned after %v, want prompt return", elapsed)
|
||||
}
|
||||
if got := dispatcher.calls.Load(); got != 1 {
|
||||
t.Fatalf("Home RPOP calls = %d, want 1", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPickHomeDispatchSelectionReleasesAccountedScopeAfterAuthValidationFailure(t *testing.T) {
|
||||
dispatcher := &fixtureHomeDispatcher{payload: []byte(`{"concurrency":{"accounted":true,"credential_id":"cred-1","model":"gpt"},"auth":{"id":"","provider":"codex"}}`)}
|
||||
manager := newHomeSelectionTestManager(t, dispatcher)
|
||||
|
||||
selection, errPick := manager.pickHomeDispatchSelection(context.Background(), "gpt", cliproxyexecutor.Options{})
|
||||
if selection != nil || errPick == nil {
|
||||
t.Fatalf("selection=%#v error=%v", selection, errPick)
|
||||
}
|
||||
if dispatcher.closedForAmbiguity {
|
||||
t.Fatal("accounted local auth validation failure fenced Home")
|
||||
}
|
||||
if freeze := manager.HomeDispatchBundle().registry.FreezeInFlight(time.Now()); len(freeze.Executions) != 0 {
|
||||
t.Fatalf("scope was not released: %#v", freeze)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPickHomeDispatchSelectionReleasesAccountedScopeAfterPayloadDecodeFailure(t *testing.T) {
|
||||
dispatcher := &fixtureHomeDispatcher{payload: []byte(`{"concurrency":{"accounted":true,"credential_id":"cred-1","model":"gpt"},"model":123,"auth":{"id":"cred-1","provider":"codex"}}`)}
|
||||
manager := newHomeSelectionTestManager(t, dispatcher)
|
||||
|
||||
selection, errPick := manager.pickHomeDispatchSelection(context.Background(), "gpt", cliproxyexecutor.Options{})
|
||||
if selection != nil || errPick == nil {
|
||||
t.Fatalf("selection=%#v error=%v", selection, errPick)
|
||||
}
|
||||
if dispatcher.closedForAmbiguity {
|
||||
t.Fatal("accounted payload decode failure fenced Home")
|
||||
}
|
||||
if freeze := manager.HomeDispatchBundle().registry.FreezeInFlight(time.Now()); len(freeze.Executions) != 0 {
|
||||
t.Fatalf("scope was not released: %#v", freeze)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPickHomeDispatchSelectionRejectsMalformedErrorPresence(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
payload string
|
||||
wantCode string
|
||||
wantFence bool
|
||||
}{
|
||||
{name: "string without tuple", payload: `{"error":"busy","auth":{"id":"cred-1","provider":"codex"}}`, wantCode: "invalid_auth"},
|
||||
{name: "empty object without tuple", payload: `{"error":{},"auth":{"id":"cred-1","provider":"codex"}}`, wantCode: "invalid_auth"},
|
||||
{name: "null without tuple", payload: `{"error":null,"auth":{"id":"cred-1","provider":"codex"}}`, wantCode: "invalid_auth"},
|
||||
{name: "empty type and code without tuple", payload: `{"error":{"type":" ","code":""},"auth":{"id":"cred-1","provider":"codex"}}`, wantCode: "invalid_auth"},
|
||||
{name: "string with tuple", payload: `{"concurrency":{"accounted":true,"credential_id":"cred-1","model":"gpt"},"error":"busy","auth":{"id":"cred-1","provider":"codex"}}`, wantCode: "invalid_home_concurrency", wantFence: true},
|
||||
{name: "empty object with tuple", payload: `{"concurrency":{"accounted":true,"credential_id":"cred-1","model":"gpt"},"error":{},"auth":{"id":"cred-1","provider":"codex"}}`, wantCode: "invalid_home_concurrency", wantFence: true},
|
||||
{name: "null with tuple", payload: `{"concurrency":{"accounted":true,"credential_id":"cred-1","model":"gpt"},"error":null,"auth":{"id":"cred-1","provider":"codex"}}`, wantCode: "invalid_home_concurrency", wantFence: true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
dispatcher := &fixtureHomeDispatcher{payload: []byte(tt.payload)}
|
||||
manager := newHomeSelectionTestManager(t, dispatcher)
|
||||
manager.executors["codex"] = schedulerTestExecutor{provider: "codex"}
|
||||
|
||||
selection, errPick := manager.pickHomeDispatchSelection(context.Background(), "gpt", cliproxyexecutor.Options{})
|
||||
if selection != nil || errPick == nil {
|
||||
t.Fatalf("selection=%#v error=%v, want malformed error rejection", selection, errPick)
|
||||
}
|
||||
var authErr *Error
|
||||
if !errors.As(errPick, &authErr) || authErr.Code != tt.wantCode {
|
||||
t.Fatalf("error=%#v, want code %q", errPick, tt.wantCode)
|
||||
}
|
||||
if dispatcher.closedForAmbiguity != tt.wantFence {
|
||||
t.Fatalf("fenced=%t, want %t", dispatcher.closedForAmbiguity, tt.wantFence)
|
||||
}
|
||||
if freeze := manager.HomeDispatchBundle().registry.FreezeInFlight(time.Now()); len(freeze.Executions) != 0 {
|
||||
t.Fatalf("scope was not released: %#v", freeze)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPickHomeDispatchSelectionValidAccountedLocalValidationReleasesAndKeepsHomeHealthy(t *testing.T) {
|
||||
validPayload := []byte(`{"concurrency":{"accounted":true,"credential_id":"cred-1","model":"gpt"},"auth_index":"cred-1","auth":{"id":"cred-1","provider":"codex"}}`)
|
||||
tests := map[string][]byte{
|
||||
"auth validation": []byte(`{"concurrency":{"accounted":true,"credential_id":"cred-1","model":"gpt"},"auth":{"id":"","provider":"codex"}}`),
|
||||
"payload decode": []byte(`{"concurrency":{"accounted":true,"credential_id":"cred-1","model":"gpt"},"model":123,"auth":{"id":"cred-1","provider":"codex"}}`),
|
||||
"auth decode": []byte(`{"concurrency":{"accounted":true,"credential_id":"cred-1","model":"gpt"},"auth":"invalid"}`),
|
||||
"identity mismatch": []byte(`{"concurrency":{"accounted":true,"credential_id":"cred-1","model":"gpt"},"auth_index":"other","auth":{"id":"cred-1","provider":"codex"}}`),
|
||||
}
|
||||
for name, invalidPayload := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
dispatcher := &fixtureHomeDispatcher{payloads: [][]byte{invalidPayload, validPayload}}
|
||||
manager := newHomeSelectionTestManager(t, dispatcher)
|
||||
manager.executors["codex"] = schedulerTestExecutor{provider: "codex"}
|
||||
releases := make(map[executionregistry.ReleaseGroup]int64)
|
||||
registry := manager.HomeDispatchBundle().registry
|
||||
registry.SetReleaseSink(func(group executionregistry.ReleaseGroup, sequence int64) {
|
||||
releases[group] = sequence
|
||||
})
|
||||
|
||||
selection, errPick := manager.pickHomeDispatchSelection(context.Background(), "gpt", cliproxyexecutor.Options{})
|
||||
if selection != nil || errPick == nil {
|
||||
t.Fatalf("first selection=%#v error=%v, want local validation failure", selection, errPick)
|
||||
}
|
||||
if dispatcher.closedForAmbiguity {
|
||||
t.Fatal("valid accounted local validation failure fenced Home")
|
||||
}
|
||||
group := executionregistry.ReleaseGroup{CredentialID: "cred-1", Model: "gpt"}
|
||||
if len(releases) != 1 || releases[group] != 1 {
|
||||
t.Fatalf("first releases=%#v, want exactly %v:1", releases, group)
|
||||
}
|
||||
|
||||
selection, errPick = manager.pickHomeDispatchSelection(context.Background(), "gpt", cliproxyexecutor.Options{})
|
||||
if errPick != nil || selection == nil {
|
||||
t.Fatalf("second selection=%#v error=%v, want healthy dispatch", selection, errPick)
|
||||
}
|
||||
selection.End("test_complete")
|
||||
if dispatcher.closedForAmbiguity {
|
||||
t.Fatal("second dispatch fenced Home")
|
||||
}
|
||||
if len(releases) != 1 || releases[group] != 2 {
|
||||
t.Fatalf("cumulative releases=%#v, want exactly %v:2", releases, group)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPickHomeDispatchSelectionFencesAccountedBusyError(t *testing.T) {
|
||||
dispatcher := &fixtureHomeDispatcher{payload: []byte(`{"concurrency":{"accounted":true,"credential_id":"cred-1","model":"gpt"},"error":{"type":"credential_concurrency_exceeded","message":"busy","retry_after_ms":750}}`)}
|
||||
manager := newHomeSelectionTestManager(t, dispatcher)
|
||||
abortSawScope := make(chan bool, 1)
|
||||
dispatcher.onAbort = func() {
|
||||
freeze := manager.HomeDispatchBundle().registry.FreezeInFlight(time.Now())
|
||||
abortSawScope <- len(freeze.Executions) == 1
|
||||
}
|
||||
|
||||
selection, errPick := manager.pickHomeDispatchSelection(context.Background(), "gpt", cliproxyexecutor.Options{})
|
||||
if selection != nil || errPick == nil {
|
||||
t.Fatalf("selection=%#v error=%v", selection, errPick)
|
||||
}
|
||||
var busy *HomeConcurrencyBusyError
|
||||
if errors.As(errPick, &busy) {
|
||||
t.Fatalf("accounted error returned ordinary busy response: %v", errPick)
|
||||
}
|
||||
if !dispatcher.closedForAmbiguity {
|
||||
t.Fatal("accounted busy error did not fence Home")
|
||||
}
|
||||
if sawScope := <-abortSawScope; !sawScope {
|
||||
t.Fatal("accounted scope ended before Home dispatch was aborted")
|
||||
}
|
||||
if freeze := manager.HomeDispatchBundle().registry.FreezeInFlight(time.Now()); len(freeze.Executions) != 0 {
|
||||
t.Fatalf("scope was not released: %#v", freeze)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMalformedAccountedTupleClosesHomeClient(t *testing.T) {
|
||||
dispatcher := &fixtureHomeDispatcher{payload: []byte(`{"concurrency":{"accounted":true,"credential_id":"cred-1","model":""},"auth":{"id":"cred-1","provider":"codex"}}`)}
|
||||
manager := newHomeSelectionTestManager(t, dispatcher)
|
||||
|
||||
selection, errPick := manager.pickHomeDispatchSelection(context.Background(), "gpt", cliproxyexecutor.Options{})
|
||||
if selection != nil || errPick == nil || !dispatcher.closedForAmbiguity {
|
||||
t.Fatalf("selection=%#v error=%v closed=%t", selection, errPick, dispatcher.closedForAmbiguity)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConcurrencyDispatchFixture(t *testing.T) {
|
||||
t.Run("accounted", func(t *testing.T) {
|
||||
raw, errRead := os.ReadFile("../../../internal/home/testdata/concurrency_dispatch_accounted.json")
|
||||
if errRead != nil {
|
||||
t.Fatalf("ReadFile(accounted fixture) error = %v", errRead)
|
||||
}
|
||||
|
||||
var fixture struct {
|
||||
Model string `json:"model"`
|
||||
Provider string `json:"provider"`
|
||||
AuthIndex string `json:"auth_index"`
|
||||
Auth struct {
|
||||
ID string `json:"id"`
|
||||
Provider string `json:"provider"`
|
||||
} `json:"auth"`
|
||||
Concurrency homeConcurrencyTuple `json:"concurrency"`
|
||||
}
|
||||
if errUnmarshal := json.Unmarshal(raw, &fixture); errUnmarshal != nil {
|
||||
t.Fatalf("Unmarshal(accounted fixture) error = %v", errUnmarshal)
|
||||
}
|
||||
wantTuple := homeConcurrencyTuple{Accounted: true, CredentialID: "cred-1", Model: "gpt"}
|
||||
if fixture.Concurrency != wantTuple {
|
||||
t.Fatalf("accounted concurrency = %#v, want %#v", fixture.Concurrency, wantTuple)
|
||||
}
|
||||
if fixture.Model != "gpt" || fixture.Provider != "codex" || fixture.AuthIndex != "cred-1" || fixture.Auth.ID != "cred-1" || fixture.Auth.Provider != "codex" {
|
||||
t.Fatalf("accounted identity model=%q provider=%q auth_index=%q auth=%#v", fixture.Model, fixture.Provider, fixture.AuthIndex, fixture.Auth)
|
||||
}
|
||||
|
||||
envelope, errEnvelope := decodeHomeDispatchConcurrencyEnvelope(raw)
|
||||
if errEnvelope != nil {
|
||||
t.Fatalf("decodeHomeDispatchConcurrencyEnvelope(accounted fixture) error = %v", errEnvelope)
|
||||
}
|
||||
if !envelope.Present || envelope.Tuple != wantTuple {
|
||||
t.Fatalf("accounted envelope = %#v, want present tuple %#v", envelope, wantTuple)
|
||||
}
|
||||
|
||||
dispatcher := &fixtureHomeDispatcher{payload: raw}
|
||||
manager := newHomeSelectionTestManager(t, dispatcher)
|
||||
manager.RegisterExecutor(schedulerTestExecutor{provider: "codex"})
|
||||
selection, errPick := manager.pickHomeDispatchSelection(context.Background(), "gpt", cliproxyexecutor.Options{})
|
||||
if errPick != nil || selection == nil {
|
||||
t.Fatalf("pickHomeDispatchSelection(accounted fixture) selection=%#v error=%v", selection, errPick)
|
||||
}
|
||||
defer selection.End("fixture_complete")
|
||||
if selection.Auth == nil || selection.Auth.ID != "cred-1" || selection.Auth.Index != "cred-1" || selection.Auth.Provider != "codex" {
|
||||
t.Fatalf("selected auth = %#v", selection.Auth)
|
||||
}
|
||||
|
||||
bundle := manager.HomeDispatchBundle()
|
||||
if bundle == nil || bundle.registry == nil {
|
||||
t.Fatal("accounted fixture did not retain a Home dispatch registry")
|
||||
}
|
||||
freeze := bundle.registry.FreezeInFlight(time.Now())
|
||||
if len(freeze.Executions) != 1 {
|
||||
t.Fatalf("accounted fixture executions = %#v", freeze.Executions)
|
||||
}
|
||||
gotScope := freeze.Executions[0]
|
||||
if !gotScope.Accounted || gotScope.CredentialID != "cred-1" || gotScope.Model != "gpt" {
|
||||
t.Fatalf("accounted fixture scope = %#v", gotScope)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("busy", func(t *testing.T) {
|
||||
raw, errRead := os.ReadFile("../../../internal/home/testdata/concurrency_dispatch_busy.json")
|
||||
if errRead != nil {
|
||||
t.Fatalf("ReadFile(busy fixture) error = %v", errRead)
|
||||
}
|
||||
|
||||
var fixture struct {
|
||||
Error *struct {
|
||||
Type string `json:"type"`
|
||||
Message string `json:"message"`
|
||||
Retryable bool `json:"retryable"`
|
||||
RetryAfterMS int64 `json:"retry_after_ms"`
|
||||
} `json:"error"`
|
||||
}
|
||||
if errUnmarshal := json.Unmarshal(raw, &fixture); errUnmarshal != nil {
|
||||
t.Fatalf("Unmarshal(busy fixture) error = %v", errUnmarshal)
|
||||
}
|
||||
if fixture.Error == nil {
|
||||
t.Fatal("busy fixture has no error object")
|
||||
}
|
||||
if fixture.Error.Type != "credential_concurrency_exceeded" || fixture.Error.Message != "credential concurrency limit reached" || !fixture.Error.Retryable || fixture.Error.RetryAfterMS != 750 {
|
||||
t.Fatalf("busy fixture error = %#v", fixture.Error)
|
||||
}
|
||||
|
||||
errBusy := decodeHomeDispatchError(raw)
|
||||
var busy *HomeConcurrencyBusyError
|
||||
if !errors.As(errBusy, &busy) || busy == nil {
|
||||
t.Fatalf("decodeHomeDispatchError(busy fixture) error = %#v, want *HomeConcurrencyBusyError", errBusy)
|
||||
}
|
||||
if got := busy.StatusCode(); got != http.StatusTooManyRequests {
|
||||
t.Fatalf("busy status = %d, want %d", got, http.StatusTooManyRequests)
|
||||
}
|
||||
retryAfter := busy.RetryAfter()
|
||||
if retryAfter == nil || *retryAfter != 750*time.Millisecond {
|
||||
t.Fatalf("busy retry after = %v, want 750ms", retryAfter)
|
||||
}
|
||||
var cause *Error
|
||||
if !errors.As(errBusy, &cause) || cause == nil || cause.Code != fixture.Error.Type || cause.Message != fixture.Error.Message || !cause.Retryable || cause.HTTPStatus != http.StatusTooManyRequests {
|
||||
t.Fatalf("busy typed cause = %#v", cause)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestHomeBusyErrorMaps429AndRetryAfter(t *testing.T) {
|
||||
errBusy := decodeHomeDispatchError([]byte(`{"error":{"type":"credential_concurrency_exceeded","message":"busy","retryable":true,"retry_after_ms":750}}`))
|
||||
statusError, ok := errBusy.(interface{ StatusCode() int })
|
||||
if !ok || statusError.StatusCode() != http.StatusTooManyRequests {
|
||||
t.Fatalf("error = %#v", errBusy)
|
||||
}
|
||||
retryError, ok := errBusy.(interface{ RetryAfter() *time.Duration })
|
||||
if !ok || retryError.RetryAfter() == nil || *retryError.RetryAfter() != 750*time.Millisecond {
|
||||
t.Fatalf("retry after = %v", retryError.RetryAfter())
|
||||
}
|
||||
}
|
||||
|
||||
func TestHomeNoCandidateErrorsMapToServiceUnavailable(t *testing.T) {
|
||||
for _, code := range []string{"auth_not_found", "auth_unavailable"} {
|
||||
t.Run(code, func(t *testing.T) {
|
||||
errDispatch := decodeHomeDispatchError([]byte(fmt.Sprintf(`{"error":{"type":%q,"message":"no auth available"}}`, code)))
|
||||
var authErr *Error
|
||||
if !errors.As(errDispatch, &authErr) || authErr.Code != code || authErr.HTTPStatus != http.StatusServiceUnavailable {
|
||||
t.Fatalf("decodeHomeDispatchError(%s) = %#v, want 503", code, errDispatch)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHomeConcurrencyTupleAuthMismatchEndsScope(t *testing.T) {
|
||||
dispatcher := &fixtureHomeDispatcher{payload: []byte(`{"concurrency":{"accounted":true,"credential_id":"cred-1","model":"gpt"},"auth_index":"other","auth":{"id":"cred-1","provider":"codex"}}`)}
|
||||
manager := newHomeSelectionTestManager(t, dispatcher)
|
||||
manager.executors["codex"] = schedulerTestExecutor{provider: "codex"}
|
||||
|
||||
selection, errPick := manager.pickHomeDispatchSelection(context.Background(), "gpt", cliproxyexecutor.Options{})
|
||||
if selection != nil || errPick == nil {
|
||||
t.Fatalf("selection=%#v error=%v", selection, errPick)
|
||||
}
|
||||
if dispatcher.closedForAmbiguity {
|
||||
t.Fatal("accounted auth identity mismatch fenced Home")
|
||||
}
|
||||
if freeze := manager.HomeDispatchBundle().registry.FreezeInFlight(time.Now()); len(freeze.Executions) != 0 {
|
||||
t.Fatalf("scope was not released: %#v", freeze)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOldHomeDispatchIsUnaccounted(t *testing.T) {
|
||||
dispatcher := &fixtureHomeDispatcher{payload: []byte(`{"auth":{"id":"cred-1","provider":"codex"}}`)}
|
||||
manager := newHomeSelectionTestManager(t, dispatcher)
|
||||
manager.executors["codex"] = schedulerTestExecutor{provider: "codex"}
|
||||
|
||||
selection, errPick := manager.pickHomeDispatchSelection(context.Background(), "gpt", cliproxyexecutor.Options{})
|
||||
if errPick != nil || selection == nil {
|
||||
t.Fatalf("selection=%#v error=%v", selection, errPick)
|
||||
}
|
||||
defer selection.End("test")
|
||||
freeze := manager.HomeDispatchBundle().registry.FreezeInFlight(time.Now())
|
||||
if len(freeze.Executions) != 1 || freeze.Executions[0].Accounted {
|
||||
t.Fatalf("old Home dispatch freeze = %#v", freeze)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHomeBusyErrorHeadersRoundUpMilliseconds(t *testing.T) {
|
||||
errBusy := decodeHomeDispatchError([]byte(`{"error":{"type":"credential_concurrency_exceeded","message":"busy","retry_after_ms":750}}`))
|
||||
headers, ok := errBusy.(interface{ SafeResponseHeaders() http.Header })
|
||||
if !ok {
|
||||
t.Fatalf("error has no safe headers: %#v", errBusy)
|
||||
}
|
||||
if got := headers.SafeResponseHeaders().Get("Retry-After"); got != "1" {
|
||||
t.Fatalf("Retry-After = %q, want 1", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstallHomeConcurrencyScopeRejectsNonCanonicalTuple(t *testing.T) {
|
||||
registry := executionregistry.New()
|
||||
pending, errBegin := registry.BeginDispatch()
|
||||
if errBegin != nil {
|
||||
t.Fatal(errBegin)
|
||||
}
|
||||
defer pending.End()
|
||||
|
||||
_, errInstall := installHomeConcurrencyScope(registry, pending, homeConcurrencyTuple{
|
||||
Accounted: true, CredentialID: " cred-1 ", Model: "gpt",
|
||||
}, executionregistry.ScopeSpec{Kind: "http", StartedAt: time.Now()})
|
||||
if !errors.Is(errInstall, ErrMalformedHomeConcurrencyTuple) {
|
||||
t.Fatalf("install error = %v, want malformed tuple", errInstall)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPickHomeDispatchSelectionFencesInvalidExplicitConcurrency(t *testing.T) {
|
||||
tests := []string{
|
||||
`{"concurrency":{"accounted":false,"credential_id":"cred-1","model":"gpt"},"auth":{"id":"cred-1","provider":"codex"}}`,
|
||||
`{"concurrency":{"accounted":true,"credential_id":" cred-1","model":"gpt"},"auth":{"id":"cred-1","provider":"codex"}}`,
|
||||
`{"concurrency":{"accounted":true,"credential_id":"cred-1","model":"other"},"model":"gpt","auth":{"id":"cred-1","provider":"codex"}}`,
|
||||
}
|
||||
for _, payload := range tests {
|
||||
dispatcher := &fixtureHomeDispatcher{payload: []byte(payload)}
|
||||
manager := newHomeSelectionTestManager(t, dispatcher)
|
||||
|
||||
selection, errPick := manager.pickHomeDispatchSelection(context.Background(), "gpt", cliproxyexecutor.Options{})
|
||||
if selection != nil || errPick == nil || !dispatcher.closedForAmbiguity {
|
||||
t.Fatalf("payload=%s selection=%#v error=%v closed=%t", payload, selection, errPick, dispatcher.closedForAmbiguity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPickHomeDispatchSelectionReleasesAccountedScopeAfterAuthDecodeFailure(t *testing.T) {
|
||||
dispatcher := &fixtureHomeDispatcher{payload: []byte(`{"concurrency":{"accounted":true,"credential_id":"cred-1","model":"gpt"},"auth":"invalid"}`)}
|
||||
manager := newHomeSelectionTestManager(t, dispatcher)
|
||||
|
||||
selection, errPick := manager.pickHomeDispatchSelection(context.Background(), "gpt", cliproxyexecutor.Options{})
|
||||
if selection != nil || errPick == nil || dispatcher.closedForAmbiguity {
|
||||
t.Fatalf("selection=%#v error=%v closed=%t", selection, errPick, dispatcher.closedForAmbiguity)
|
||||
}
|
||||
if freeze := manager.HomeDispatchBundle().registry.FreezeInFlight(time.Now()); len(freeze.Executions) != 0 {
|
||||
t.Fatalf("scope was not released: %#v", freeze)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHomeConcurrencyBusyErrorsRemainTypedWhenWrapped(t *testing.T) {
|
||||
for _, code := range []string{"credential_concurrency_exceeded", "credential_model_concurrency_exceeded"} {
|
||||
errBusy := decodeHomeDispatchError([]byte(fmt.Sprintf(`{"error":{"type":%q,"message":"busy","retryable":false}}`, code)))
|
||||
var busy *HomeConcurrencyBusyError
|
||||
if !errors.As(errBusy, &busy) {
|
||||
t.Fatalf("code=%s error=%#v, want typed busy error", code, errBusy)
|
||||
}
|
||||
if busy.RetryAfter() != nil {
|
||||
t.Fatalf("code=%s retry after = %v, want nil", code, busy.RetryAfter())
|
||||
}
|
||||
var cause *Error
|
||||
if !errors.As(fmt.Errorf("wrapped: %w", errBusy), &cause) || cause.Code != code || cause.Retryable {
|
||||
t.Fatalf("code=%s cause=%#v", code, cause)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetryAfterFromWrappedHomeBusyError(t *testing.T) {
|
||||
errBusy := NewHomeConcurrencyBusyError("busy", 750*time.Millisecond)
|
||||
if got := retryAfterFromError(fmt.Errorf("wrapped: %w", errBusy)); got == nil || *got != 750*time.Millisecond {
|
||||
t.Fatalf("retry after = %v, want 750ms", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCanonicalHomeConcurrencyModelKeyMatchesHomeLimiter(t *testing.T) {
|
||||
cases := map[string]string{
|
||||
" gpt(high) ": "gpt",
|
||||
"gpt(8192)": "gpt",
|
||||
"gpt(-1)": "gpt",
|
||||
" GPT(AUTO) ": "gpt",
|
||||
"model(custom)": "model(custom)",
|
||||
"model(+1)": "model(+1)",
|
||||
"model(2147483648)": "model(2147483648)",
|
||||
"(high)": "(high)",
|
||||
}
|
||||
for input, want := range cases {
|
||||
if got := canonicalHomeConcurrencyModelKey(input); got != want {
|
||||
t.Fatalf("canonicalHomeConcurrencyModelKey(%q) = %q, want %q", input, got, want)
|
||||
}
|
||||
}
|
||||
if got := canonicalHomeConcurrencyModelKey("gpt\xff(high)"); got != "" {
|
||||
t.Fatalf("canonicalHomeConcurrencyModelKey() = %q, want empty for malformed UTF-8", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccountedHomeConcurrencyTupleRequiresCanonicalLimiterModel(t *testing.T) {
|
||||
for _, model := range []string{"GPT", "gpt(high)", "model(custom) "} {
|
||||
errValidate := validateAccountedHomeConcurrencyTuple(homeConcurrencyTuple{Accounted: true, CredentialID: "cred-1", Model: model})
|
||||
if !errors.Is(errValidate, ErrMalformedHomeConcurrencyTuple) {
|
||||
t.Fatalf("model=%q validation error = %v, want malformed tuple", model, errValidate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHomeConcurrencyTupleStringsAreValidUTF8(t *testing.T) {
|
||||
if utf8.ValidString(string([]byte{0xff})) {
|
||||
t.Fatal("test setup expected invalid UTF-8")
|
||||
}
|
||||
if _, errDecode := decodeHomeDispatchConcurrencyEnvelope([]byte{'{', 0xff, '}'}); errDecode == nil {
|
||||
t.Fatal("raw non-UTF-8 Home envelope was accepted")
|
||||
}
|
||||
if !errors.Is(validateAccountedHomeConcurrencyTuple(homeConcurrencyTuple{Accounted: true, CredentialID: string([]byte{0xff}), Model: "gpt"}), ErrMalformedHomeConcurrencyTuple) {
|
||||
t.Fatal("invalid UTF-8 credential was accepted")
|
||||
}
|
||||
if !errors.Is(validateAccountedHomeConcurrencyTuple(homeConcurrencyTuple{Accounted: true, CredentialID: "cred-1", Model: strings.Repeat("g", 257)}), ErrMalformedHomeConcurrencyTuple) {
|
||||
t.Fatal("oversized model was accepted")
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue