mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-09-06 16:15:50 +08:00
Track when executor calls cross an upstream transport boundary and use that signal to keep model/provider errors from being replaced by later local preparation, selection, or internal failures. Mark HTTP, websocket, relay, and usage-tracked transports as upstream attempts, while avoiding marks for local validation, logging, missing sessions, and successful websocket handshakes before request send. Parse relative auth expiry metadata and adjust Antigravity refresh timing.
158 lines
5.0 KiB
Go
158 lines
5.0 KiB
Go
package helps
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
|
|
"github.com/router-for-me/CLIProxyAPI/v7/internal/logging"
|
|
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor"
|
|
)
|
|
|
|
func TestRequestLoggingDoesNotMarkUpstreamAttempt(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
record func(context.Context)
|
|
}{
|
|
{
|
|
name: "HTTP",
|
|
record: func(ctx context.Context) {
|
|
RecordAPIRequest(ctx, &config.Config{}, UpstreamRequestLog{URL: "https://api.example.com", Method: http.MethodPost})
|
|
},
|
|
},
|
|
{
|
|
name: "websocket",
|
|
record: func(ctx context.Context) {
|
|
RecordAPIWebsocketRequest(ctx, &config.Config{}, UpstreamRequestLog{URL: "wss://api.example.com", Method: "WEBSOCKET"})
|
|
},
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
ctx := cliproxyexecutor.WithUpstreamAttemptTracker(context.Background())
|
|
test.record(ctx)
|
|
if cliproxyexecutor.UpstreamAttempted(ctx) {
|
|
t.Fatal("request logging marked an upstream attempt before transport")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRecordAPIRequestClonesDeferredBodyWhenRequestLogDisabled(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
recorder := httptest.NewRecorder()
|
|
ginCtx, _ := gin.CreateTestContext(recorder)
|
|
ctx := context.WithValue(context.Background(), "gin", ginCtx)
|
|
body := []byte(`{"model":"original"}`)
|
|
|
|
RecordAPIRequest(ctx, &config.Config{}, UpstreamRequestLog{
|
|
URL: "https://api.example.com/v1/responses",
|
|
Method: http.MethodPost,
|
|
Body: body,
|
|
})
|
|
body[10] = 'X'
|
|
|
|
value, exists := ginCtx.Get(logging.DeferredAPIRequestContextKey)
|
|
if !exists {
|
|
t.Fatal("deferred API request was not captured")
|
|
}
|
|
requests, ok := value.([]logging.DeferredAPIRequest)
|
|
if !ok || len(requests) != 1 {
|
|
t.Fatalf("deferred API requests = %#v, want one request", value)
|
|
}
|
|
captured := string(requests[0]())
|
|
if !strings.Contains(captured, `{"model":"original"}`) {
|
|
t.Fatalf("captured API request = %q, want original body", captured)
|
|
}
|
|
}
|
|
|
|
func TestRecordAPIResponseMetadataStoresHeadersWhenRequestLogDisabled(t *testing.T) {
|
|
ctx := logging.WithResponseHeadersHolder(context.Background())
|
|
headers := http.Header{}
|
|
headers.Add("X-Upstream-Request-Id", "upstream-req-1")
|
|
|
|
RecordAPIResponseMetadata(ctx, &config.Config{}, http.StatusOK, headers)
|
|
headers.Set("X-Upstream-Request-Id", "mutated")
|
|
|
|
got := logging.GetResponseHeaders(ctx)
|
|
if got.Get("X-Upstream-Request-Id") != "upstream-req-1" {
|
|
t.Fatalf("response header = %q, want %q", got.Get("X-Upstream-Request-Id"), "upstream-req-1")
|
|
}
|
|
}
|
|
|
|
func TestAPIResponseAttemptsAreSeparated(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
tests := []struct {
|
|
name string
|
|
fileBacked bool
|
|
firstResponseBody []byte
|
|
}{
|
|
{name: "memory backed error"},
|
|
{name: "file backed error", fileBacked: true},
|
|
{name: "memory backed partial body", firstResponseBody: []byte("partial")},
|
|
{name: "file backed partial body", fileBacked: true, firstResponseBody: []byte("partial")},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
recorder := httptest.NewRecorder()
|
|
ginCtx, _ := gin.CreateTestContext(recorder)
|
|
var responseSource *logging.FileBodySource
|
|
if tt.fileBacked {
|
|
var errSource error
|
|
responseSource, errSource = logging.NewFileBodySourceInDir(t.TempDir(), "api-response")
|
|
if errSource != nil {
|
|
t.Fatalf("NewFileBodySourceInDir: %v", errSource)
|
|
}
|
|
t.Cleanup(func() {
|
|
if errCleanup := responseSource.Cleanup(); errCleanup != nil {
|
|
t.Errorf("Cleanup: %v", errCleanup)
|
|
}
|
|
})
|
|
ginCtx.Set(logging.APIResponseSourceContextKey, responseSource)
|
|
}
|
|
|
|
ctx := context.WithValue(context.Background(), "gin", ginCtx)
|
|
cfg := &config.Config{SDKConfig: config.SDKConfig{RequestLog: true}}
|
|
RecordAPIRequest(ctx, cfg, UpstreamRequestLog{URL: "https://api.example.com/first", Method: http.MethodPost})
|
|
if len(tt.firstResponseBody) > 0 {
|
|
AppendAPIResponseChunk(ctx, cfg, tt.firstResponseBody)
|
|
} else {
|
|
RecordAPIResponseError(ctx, cfg, errors.New("EOF"))
|
|
}
|
|
RecordAPIRequest(ctx, cfg, UpstreamRequestLog{URL: "https://api.example.com/second", Method: http.MethodPost})
|
|
RecordAPIResponseError(ctx, cfg, errors.New("retry failed"))
|
|
|
|
var response []byte
|
|
if responseSource != nil {
|
|
var errBytes error
|
|
response, errBytes = responseSource.Bytes()
|
|
if errBytes != nil {
|
|
t.Fatalf("responseSource.Bytes: %v", errBytes)
|
|
}
|
|
} else {
|
|
value, exists := ginCtx.Get(apiResponseKey)
|
|
if !exists {
|
|
t.Fatal("API_RESPONSE was not captured")
|
|
}
|
|
response, _ = value.([]byte)
|
|
}
|
|
|
|
previousEnd := "Error: EOF"
|
|
if len(tt.firstResponseBody) > 0 {
|
|
previousEnd = string(tt.firstResponseBody)
|
|
}
|
|
wantBoundary := previousEnd + "\n\n=== API RESPONSE 2 ==="
|
|
if !strings.Contains(string(response), wantBoundary) {
|
|
t.Fatalf("API response attempts are not separated by one blank line:\n%q\nwant boundary %q", response, wantBoundary)
|
|
}
|
|
})
|
|
}
|
|
}
|