test/codex_refresh_recovery_test.go
Rama main · réplica de lectura del repositorio original.
package test
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"net/http/httptest"
"net/url"
"sync/atomic"
"testing"
"time"
"github.com/router-for-me/CLIProxyAPI/v8/internal/config"
"github.com/router-for-me/CLIProxyAPI/v8/internal/registry"
runtimeexecutor "github.com/router-for-me/CLIProxyAPI/v8/internal/runtime/executor"
_ "github.com/router-for-me/CLIProxyAPI/v8/internal/translator"
coreauth "github.com/router-for-me/CLIProxyAPI/v8/sdk/cliproxy/auth"
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v8/sdk/cliproxy/executor"
sdktranslator "github.com/router-for-me/CLIProxyAPI/v8/sdk/translator"
)
type codexRecoveryTransport struct {
target *url.URL
base http.RoundTripper
}
func (t codexRecoveryTransport) RoundTrip(req *http.Request) (*http.Response, error) {
local := req.Clone(req.Context())
local.URL.Scheme = t.target.Scheme
local.URL.Host = t.target.Host
return t.base.RoundTrip(local)
}
func TestCodexSIWCRecoveryPreservesRequestPartition(t *testing.T) {
for _, tc := range []struct {
name string
missingSIWC bool
siwcStatus int
nativeAllowed bool
}{
{name: "expired SIWC", siwcStatus: http.StatusServiceUnavailable, nativeAllowed: true},
{name: "missing SIWC with healthy model state", missingSIWC: true, siwcStatus: http.StatusServiceUnavailable, nativeAllowed: true},
{name: "explicit SIWC rejection", siwcStatus: http.StatusBadRequest},
} {
t.Run(tc.name, func(t *testing.T) {
const model = "gpt-5.4"
var outerCalls, siwcCalls, inferenceCalls atomic.Int32
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
w.Header().Set("Content-Type", "application/json")
switch req.URL.Path {
case "/oauth/token":
outerCalls.Add(1)
_, _ = io.WriteString(w, `{"access_token":"outer-rotated","refresh_token":"outer-next","expires_in":86400}`)
case "/api/accounts/oauth/token":
siwcCalls.Add(1)
w.WriteHeader(tc.siwcStatus)
body := `{"error":"server_error"}`
if tc.siwcStatus == http.StatusBadRequest {
body = `{"error":"invalid_grant","code":"refresh_token_reused"}`
}
_, _ = io.WriteString(w, body)
default:
inferenceCalls.Add(1)
if got := req.Header.Get("Authorization"); got != "Bearer outer-rotated" {
t.Errorf("inference used %q, want the refreshed outer token", got)
w.WriteHeader(http.StatusUnauthorized)
return
}
w.Header().Set("Content-Type", "text/event-stream")
_, _ = fmt.Fprint(w, "data: {\"type\":\"response.completed\",\"response\":{\"id\":\"recovery-response\",\"status\":\"completed\",\"model\":\"gpt-5.4\",\"output\":[],\"usage\":{\"input_tokens\":1,\"output_tokens\":1,\"total_tokens\":2}}}\n\n")
}
}))
t.Cleanup(server.Close)
target, errParse := url.Parse(server.URL)
if errParse != nil {
t.Fatal(errParse)
}
transport := &http.Transport{}
t.Cleanup(transport.CloseIdleConnections)
originalTransport := http.DefaultTransport
http.DefaultTransport = codexRecoveryTransport{target: target, base: transport}
t.Cleanup(func() { http.DefaultTransport = originalTransport })
cfg := &config.Config{AuthDir: t.TempDir()}
manager := coreauth.NewManager(nil, &coreauth.RoundRobinSelector{}, nil)
manager.SetRetryConfig(0, 0, 0)
manager.RegisterExecutor(runtimeexecutor.NewCodexExecutor(cfg))
authID := t.Name()
registry.GetGlobalRegistry().RegisterClient(authID, "codex", []*registry.ModelInfo{{ID: model}})
t.Cleanup(func() { registry.GetGlobalRegistry().UnregisterClient(authID) })
now := time.Now()
siwc := map[string]any{"refresh_token": authID + "-siwc", "client_id": "issued-client"}
if !tc.missingSIWC {
siwc["access_token"] = "siwc-expired"
siwc["expires_at"] = now.Add(-time.Hour).Format(time.RFC3339)
}
auth := &coreauth.Auth{ID: authID, Provider: "codex", Status: coreauth.StatusActive,
Attributes: map[string]string{"base_url": server.URL},
Metadata: map[string]any{
"access_token": "outer-before", "refresh_token": authID + "-outer", "expired": now.Add(time.Hour).Format(time.RFC3339),
"use_siwc": true, "siwc": siwc,
},
}
if tc.missingSIWC {
auth.ModelStates = map[string]*coreauth.ModelState{model: {Status: coreauth.StatusActive}}
}
if _, errRegister := manager.Register(context.Background(), auth); errRegister != nil {
t.Fatal(errRegister)
}
if _, errRefresh := manager.ForceRefreshAuth(context.Background(), authID); errRefresh != nil {
t.Fatal(errRefresh)
}
if outerCalls.Load() != 1 || siwcCalls.Load() != 1 {
t.Fatalf("refresh HTTP calls = outer %d / SIWC %d, want 1 / 1", outerCalls.Load(), siwcCalls.Load())
}
recovered, _ := manager.GetByID(authID)
if !recovered.RequiresSignIn || recovered.RefreshError == nil {
t.Fatal("the unresolved or rejected SIWC grant lost its recovery requirement")
}
request := cliproxyexecutor.Request{Model: model, Payload: []byte(`{"model":"gpt-5.4","input":"hello"}`)}
assertSIWCBlocked := func() {
t.Helper()
_, errExecute := manager.Execute(context.Background(), []string{"codex"}, request, cliproxyexecutor.Options{SourceFormat: sdktranslator.FormatOpenAIResponse})
var authErr *coreauth.Error
if !errors.As(errExecute, &authErr) || authErr.Code != "auth_unavailable" {
t.Fatalf("unusable SIWC credential reached execution: %v", errExecute)
}
}
assertSIWCBlocked()
_, errNative := manager.Execute(context.Background(), []string{"codex"}, request, cliproxyexecutor.Options{
SourceFormat: sdktranslator.FormatOpenAIResponse,
Headers: http.Header{"User-Agent": {"codex_cli_rs/1.0.0"}},
})
if (errNative == nil) != tc.nativeAllowed {
t.Fatalf("native outer inference: error=%v, want allowed=%v", errNative, tc.nativeAllowed)
}
assertSIWCBlocked()
_, _ = manager.ForceRefreshAuth(context.Background(), authID)
if siwcCalls.Load() != 1 {
t.Fatal("a later refresh resubmitted the fenced SIWC grant")
}
assertSIWCBlocked()
wantInference := int32(0)
if tc.nativeAllowed {
wantInference = 1
}
if inferenceCalls.Load() != wantInference {
t.Fatalf("inference calls = %d, want %d", inferenceCalls.Load(), wantInference)
}
})
}
}