2026-02-22 13:31:30 +08:00
package handler
import (
2026-07-18 00:11:18 +08:00
"context"
2026-02-22 13:31:30 +08:00
"net/http"
"net/http/httptest"
2026-07-18 00:11:18 +08:00
"strings"
2026-02-22 13:31:30 +08:00
"sync"
"testing"
2026-07-18 00:11:18 +08:00
"unicode/utf8"
2026-02-22 13:31:30 +08:00
2026-08-19 09:03:37 -07:00
"github.com/Wei-Shaw/sub2api/internal/pkg/ctxkey"
2026-02-28 15:01:20 +08:00
middleware2 "github.com/Wei-Shaw/sub2api/internal/server/middleware"
2026-02-22 13:31:30 +08:00
"github.com/Wei-Shaw/sub2api/internal/service"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
)
2026-07-18 00:11:18 +08:00
type ingressRejectSettingRepo struct {
service . SettingRepository
getValueCalls int
}
func ( r * ingressRejectSettingRepo ) GetValue ( context . Context , string ) ( string , error ) {
r . getValueCalls ++
return "" , service . ErrSettingNotFound
}
func ( r * ingressRejectSettingRepo ) GetMultiple ( context . Context , [] string ) ( map [ string ] string , error ) {
r . getValueCalls ++
return map [ string ] string {}, nil
}
func ( r * ingressRejectSettingRepo ) Set ( context . Context , string , string ) error {
return nil
}
type ingressRejectOpsRepo struct {
service . OpsRepository
insertCalls int
2026-08-20 21:06:48 -07:00
entries [] * service . OpsInsertErrorLogInput
2026-07-18 00:11:18 +08:00
}
2026-08-20 21:06:48 -07:00
func ( r * ingressRejectOpsRepo ) InsertErrorLog ( _ context . Context , entry * service . OpsInsertErrorLogInput ) ( int64 , error ) {
2026-07-18 00:11:18 +08:00
r . insertCalls ++
2026-08-20 21:06:48 -07:00
r . entries = append ( r . entries , entry )
2026-07-18 00:11:18 +08:00
return 0 , nil
}
2026-08-20 21:06:48 -07:00
func ( r * ingressRejectOpsRepo ) BatchInsertErrorLogs ( _ context . Context , entries [] * service . OpsInsertErrorLogInput ) ( int64 , error ) {
2026-07-18 00:11:18 +08:00
r . insertCalls ++
2026-08-20 21:06:48 -07:00
r . entries = append ( r . entries , entries ... )
2026-07-18 00:11:18 +08:00
return 0 , nil
}
func TestOpsErrorLogQueueByteBudget ( t * testing . T ) {
previousBytes := opsErrorLogQueueBytes . Load ()
previousLen := opsErrorLogQueueLen . Load ()
opsErrorLogQueueBytes . Store ( 0 )
opsErrorLogQueueLen . Store ( 0 )
t . Cleanup ( func () {
opsErrorLogQueueBytes . Store ( previousBytes )
opsErrorLogQueueLen . Store ( previousLen )
})
if ! reserveOpsErrorLogQueueBytes ( opsErrorLogMaxQueueBytes - 1 ) {
t . Fatal ( "first reservation within byte budget should succeed" )
}
if reserveOpsErrorLogQueueBytes ( 2 ) {
t . Fatal ( "reservation beyond byte budget should be rejected" )
}
if got := OpsErrorLogQueueBytes (); got != opsErrorLogMaxQueueBytes - 1 {
t . Fatalf ( "queued bytes = %d, want %d" , got , opsErrorLogMaxQueueBytes - 1 )
}
if got := OpsErrorLogQueueLength (); got != 1 {
t . Fatalf ( "queue length = %d, want 1" , got )
}
}
func TestEstimateOpsErrorLogJobBytesIncludesVariablePayloads ( t * testing . T ) {
base := estimateOpsErrorLogJobBytes ( & service . OpsInsertErrorLogInput {})
message := "upstream message"
detail := "upstream detail"
events := `[{"error":"x"}]`
entry := & service . OpsInsertErrorLogInput {
ErrorBody : strings . Repeat ( "x" , 1024 ),
ErrorMessage : "client error" ,
UserAgent : "test-agent" ,
UpstreamErrorMessage : & message ,
UpstreamErrorDetail : & detail ,
UpstreamErrorsJSON : & events ,
}
if got := estimateOpsErrorLogJobBytes ( entry ); got <= base + 1024 {
t . Fatalf ( "estimated bytes = %d, expected variable payloads above %d" , got , base + 1024 )
}
}
2026-02-22 13:31:30 +08:00
func resetOpsErrorLoggerStateForTest ( t * testing . T ) {
t . Helper ()
opsErrorLogMu . Lock ()
ch := opsErrorLogQueue
opsErrorLogQueue = nil
opsErrorLogStopping = true
opsErrorLogMu . Unlock ()
if ch != nil {
close ( ch )
}
opsErrorLogWorkersWg . Wait ()
opsErrorLogOnce = sync . Once {}
opsErrorLogStopOnce = sync . Once {}
opsErrorLogWorkersWg = sync . WaitGroup {}
opsErrorLogMu = sync . RWMutex {}
opsErrorLogStopping = false
opsErrorLogQueueLen . Store ( 0 )
opsErrorLogEnqueued . Store ( 0 )
opsErrorLogDropped . Store ( 0 )
opsErrorLogProcessed . Store ( 0 )
opsErrorLogSanitized . Store ( 0 )
opsErrorLogLastDropLogAt . Store ( 0 )
opsErrorLogShutdownCh = make ( chan struct {})
opsErrorLogShutdownOnce = sync . Once {}
opsErrorLogDrained . Store ( false )
}
func TestEnqueueOpsErrorLog_QueueFullDrop ( t * testing . T ) {
resetOpsErrorLoggerStateForTest ( t )
// 禁止 enqueueOpsErrorLog 触发 workers,使用测试队列验证满队列降级。
opsErrorLogOnce . Do ( func () {})
opsErrorLogMu . Lock ()
opsErrorLogQueue = make ( chan opsErrorLogJob , 1 )
opsErrorLogMu . Unlock ()
ops := service . NewOpsService ( nil , nil , nil , nil , nil , nil , nil , nil , nil , nil , nil )
entry := & service . OpsInsertErrorLogInput { ErrorPhase : "upstream" , ErrorType : "upstream_error" }
enqueueOpsErrorLog ( ops , entry )
enqueueOpsErrorLog ( ops , entry )
require . Equal ( t , int64 ( 1 ), OpsErrorLogEnqueuedTotal ())
require . Equal ( t , int64 ( 1 ), OpsErrorLogDroppedTotal ())
require . Equal ( t , int64 ( 1 ), OpsErrorLogQueueLength ())
}
func TestEnqueueOpsErrorLog_EarlyReturnBranches ( t * testing . T ) {
resetOpsErrorLoggerStateForTest ( t )
ops := service . NewOpsService ( nil , nil , nil , nil , nil , nil , nil , nil , nil , nil , nil )
entry := & service . OpsInsertErrorLogInput { ErrorPhase : "upstream" , ErrorType : "upstream_error" }
// nil 入参分支
enqueueOpsErrorLog ( nil , entry )
enqueueOpsErrorLog ( ops , nil )
require . Equal ( t , int64 ( 0 ), OpsErrorLogEnqueuedTotal ())
// shutdown 分支
close ( opsErrorLogShutdownCh )
enqueueOpsErrorLog ( ops , entry )
require . Equal ( t , int64 ( 0 ), OpsErrorLogEnqueuedTotal ())
// stopping 分支
resetOpsErrorLoggerStateForTest ( t )
opsErrorLogMu . Lock ()
opsErrorLogStopping = true
opsErrorLogMu . Unlock ()
enqueueOpsErrorLog ( ops , entry )
require . Equal ( t , int64 ( 0 ), OpsErrorLogEnqueuedTotal ())
// queue nil 分支(防止启动 worker 干扰)
resetOpsErrorLoggerStateForTest ( t )
opsErrorLogOnce . Do ( func () {})
opsErrorLogMu . Lock ()
opsErrorLogQueue = nil
opsErrorLogMu . Unlock ()
enqueueOpsErrorLog ( ops , entry )
require . Equal ( t , int64 ( 0 ), OpsErrorLogEnqueuedTotal ())
}
2026-02-28 15:01:20 +08:00
func TestOpsCaptureWriterPool_ResetOnRelease ( t * testing . T ) {
gin . SetMode ( gin . TestMode )
rec := httptest . NewRecorder ()
c , _ := gin . CreateTestContext ( rec )
c . Request = httptest . NewRequest ( http . MethodGet , "/test" , nil )
writer := acquireOpsCaptureWriter ( c . Writer )
require . NotNil ( t , writer )
2026-08-19 23:13:40 -07:00
c . Writer . WriteHeader ( http . StatusInternalServerError )
_ , err := writer . WriteString ( "temp-error-body" )
2026-02-28 15:01:20 +08:00
require . NoError ( t , err )
2026-08-19 23:13:40 -07:00
require . NotEmpty ( t , writer . capturedBytes ())
2026-02-28 15:01:20 +08:00
releaseOpsCaptureWriter ( writer )
reused := acquireOpsCaptureWriter ( c . Writer )
defer releaseOpsCaptureWriter ( reused )
2026-08-19 23:13:40 -07:00
require . Empty ( t , reused . capturedBytes (), "writer should be reset before reuse" )
2026-02-28 15:01:20 +08:00
}
2026-07-18 00:11:18 +08:00
func TestOpsCaptureWriterPool_DropsLargeBuffers ( t * testing . T ) {
2026-08-19 23:13:40 -07:00
state := & opsCaptureWriterState {}
state . buf . Grow ( opsCaptureWriterPoolMaxRetainedCapacity + 1 )
require . False ( t , shouldPoolOpsCaptureWriterState ( state ))
2026-07-18 00:11:18 +08:00
}
func TestEnqueueOpsErrorLog_SanitizesAndBoundsBodyBeforeQueue ( t * testing . T ) {
setupOpsErrorLogTestQueue ( t , 1 )
ops := service . NewOpsService ( nil , nil , nil , nil , nil , nil , nil , nil , nil , nil , nil )
secret := strings . Repeat ( "s" , service . OpsErrorLogQueueBodyMaxBytes )
entry := & service . OpsInsertErrorLogInput {
ErrorPhase : "request" ,
ErrorType : "api_error" ,
ErrorBody : `{"authorization":"Bearer ` + secret + `","message":"failed"}` ,
}
enqueueOpsErrorLog ( ops , entry )
job := <- opsErrorLogQueue
require . LessOrEqual ( t , len ( job . entry . ErrorBody ), service . OpsErrorLogQueueBodyMaxBytes )
require . NotContains ( t , job . entry . ErrorBody , secret )
require . Equal ( t , int64 ( 1 ), OpsErrorLogSanitizedTotal ())
}
2026-02-28 15:01:20 +08:00
func TestOpsErrorLoggerMiddleware_DoesNotBreakOuterMiddlewares ( t * testing . T ) {
gin . SetMode ( gin . TestMode )
r := gin . New ()
r . Use ( middleware2 . Recovery ())
r . Use ( middleware2 . RequestLogger ())
r . Use ( middleware2 . Logger ())
r . GET ( "/v1/messages" , OpsErrorLoggerMiddleware ( nil ), func ( c * gin . Context ) {
c . Status ( http . StatusNoContent )
})
rec := httptest . NewRecorder ()
req := httptest . NewRequest ( http . MethodGet , "/v1/messages" , nil )
require . NotPanics ( t , func () {
r . ServeHTTP ( rec , req )
})
require . Equal ( t , http . StatusNoContent , rec . Code )
}
2026-02-28 10:37:06 +08:00
2026-07-08 02:51:51 +00:00
// setupOpsErrorLogTestQueue 阻止 enqueueOpsErrorLog 启动真实 worker,改用可检查的测试队列。
func setupOpsErrorLogTestQueue ( t * testing . T , size int ) {
t . Helper ()
resetOpsErrorLoggerStateForTest ( t )
opsErrorLogOnce . Do ( func () {})
opsErrorLogMu . Lock ()
opsErrorLogQueue = make ( chan opsErrorLogJob , size )
opsErrorLogMu . Unlock ()
}
2026-07-18 00:11:18 +08:00
func TestOpsErrorLoggerMiddleware_HardSkipsIngressRejection ( t * testing . T ) {
setupOpsErrorLogTestQueue ( t , 4 )
gin . SetMode ( gin . TestMode )
settings := & ingressRejectSettingRepo {}
repo := & ingressRejectOpsRepo {}
ops := service . NewOpsService ( repo , settings , nil , nil , nil , nil , nil , nil , nil , nil , nil )
// Construction may read unrelated runtime settings; only request-path reads matter here.
settings . getValueCalls = 0
router := gin . New ()
router . Use ( OpsErrorLoggerMiddleware ( ops ))
router . GET ( "/v1/messages" , func ( c * gin . Context ) {
middleware2 . MarkIngressRejected ( c , middleware2 . IngressRejectInvalidAPIKey )
c . JSON ( http . StatusUnauthorized , gin . H { "code" : "INVALID_API_KEY" , "message" : "Invalid API key" })
})
w := httptest . NewRecorder ()
req := httptest . NewRequest ( http . MethodGet , "/v1/messages" , nil )
router . ServeHTTP ( w , req )
require . Equal ( t , http . StatusUnauthorized , w . Code )
require . JSONEq ( t , `{"code":"INVALID_API_KEY","message":"Invalid API key"}` , w . Body . String ())
require . Zero ( t , settings . getValueCalls , "ingress rejection must bypass monitoring settings reads" )
require . Zero ( t , repo . insertCalls , "ingress rejection must bypass inserts" )
require . Zero ( t , OpsErrorLogEnqueuedTotal (), "ingress rejection must not enter the error queue" )
}
2026-08-19 23:13:40 -07:00
func TestOpsErrorLoggerMiddleware_DedicatedCyberSessionBlockRecordsExactlyOnce ( t * testing . T ) {
setupOpsErrorLogTestQueue ( t , 3 )
gin . SetMode ( gin . TestMode )
ops := service . NewOpsService ( nil , nil , nil , nil , nil , nil , nil , nil , nil , nil , nil )
h := & OpenAIGatewayHandler { opsService : ops }
apiKey := & service . APIKey { ID : 41 , Key : "sk-dedicated-test" }
router := gin . New ()
router . Use ( OpsErrorLoggerMiddleware ( ops ))
router . POST ( "/v1/responses" , func ( c * gin . Context ) {
h . enqueueCyberSessionBlockedOpsEntry ( c , apiKey , "gpt-test" , "session-block-hash" )
c . JSON ( http . StatusForbidden , gin . H { "error" : gin . H {
"type" : "permission_error" , "code" : "session_blocked_by_cyber_policy" , "message" : "blocked" ,
}})
})
recorder := httptest . NewRecorder ()
router . ServeHTTP ( recorder , httptest . NewRequest ( http . MethodPost , "/v1/responses" , nil ))
require . Equal ( t , http . StatusForbidden , recorder . Code )
require . Equal ( t , int64 ( 1 ), OpsErrorLogQueueLength ())
job := <- opsErrorLogQueue
require . Equal ( t , "cyber_policy_session_blocked" , job . entry . ErrorType )
require . Equal ( t , http . StatusForbidden , job . entry . StatusCode )
}
func TestOpsErrorLoggerMiddleware_OrdinaryPermissionStillRecords ( t * testing . T ) {
setupOpsErrorLogTestQueue ( t , 2 )
gin . SetMode ( gin . TestMode )
ops := service . NewOpsService ( nil , nil , nil , nil , nil , nil , nil , nil , nil , nil , nil )
router := gin . New ()
router . Use ( OpsErrorLoggerMiddleware ( ops ))
router . POST ( "/v1/responses" , func ( c * gin . Context ) {
c . JSON ( http . StatusForbidden , gin . H { "error" : gin . H {
"type" : "permission_error" , "code" : "permission_denied" , "message" : "denied" ,
}})
})
recorder := httptest . NewRecorder ()
router . ServeHTTP ( recorder , httptest . NewRequest ( http . MethodPost , "/v1/responses" , nil ))
require . Equal ( t , int64 ( 1 ), OpsErrorLogQueueLength ())
job := <- opsErrorLogQueue
require . Equal ( t , "permission_error" , job . entry . ErrorType )
require . Equal ( t , http . StatusForbidden , job . entry . StatusCode )
}
2026-08-20 21:06:48 -07:00
func TestOpsErrorLoggerMiddleware_RecordsRecoveredUpstreamTelemetryOutsideFailureSLA ( t * testing . T ) {
2026-08-19 09:03:37 -07:00
setupOpsErrorLogTestQueue ( t , 2 )
gin . SetMode ( gin . TestMode )
2026-08-20 21:06:48 -07:00
repo := & ingressRejectOpsRepo {}
ops := service . NewOpsService ( repo , nil , nil , nil , nil , nil , nil , nil , nil , nil , nil )
2026-08-19 09:03:37 -07:00
router := gin . New ()
router . Use ( OpsErrorLoggerMiddleware ( ops ))
router . POST ( "/v1/responses" , func ( c * gin . Context ) {
c . Set ( service . OpsUpstreamErrorsKey , [] * service . OpsUpstreamErrorEvent {{
UpstreamStatusCode : http . StatusTooManyRequests ,
Message : "earlier attempt was rate limited" ,
}})
c . JSON ( http . StatusOK , gin . H { "status" : "completed" })
})
recorder := httptest . NewRecorder ()
router . ServeHTTP ( recorder , httptest . NewRequest ( http . MethodPost , "/v1/responses" , nil ))
require . Equal ( t , http . StatusOK , recorder . Code )
2026-08-20 21:06:48 -07:00
require . Equal ( t , int64 ( 1 ), OpsErrorLogQueueLength ())
job := <- opsErrorLogQueue
require . Nil ( t , job . entry . UpstreamErrors , "raw attempts must be released before async queueing" )
require . NotNil ( t , job . entry . UpstreamErrorsJSON )
queuedEvents , err := service . ParseOpsUpstreamErrors ( * job . entry . UpstreamErrorsJSON )
require . NoError ( t , err )
require . Len ( t , queuedEvents , 1 )
require . Equal ( t , http . StatusTooManyRequests , queuedEvents [ 0 ]. UpstreamStatusCode )
flushOpsErrorLogBatch ([] opsErrorLogJob { job })
require . Equal ( t , 1 , repo . insertCalls )
require . Len ( t , repo . entries , 1 )
persisted := repo . entries [ 0 ]
require . Equal ( t , http . StatusOK , persisted . StatusCode , "recovered telemetry must remain outside failed-request SLA" )
require . Equal ( t , "upstream" , persisted . ErrorPhase )
require . Equal ( t , "upstream_error" , persisted . ErrorType )
require . Equal ( t , "Recovered upstream error 429: earlier attempt was rate limited" , persisted . ErrorMessage )
require . NotNil ( t , persisted . UpstreamErrorsJSON )
persistedEvents , err := service . ParseOpsUpstreamErrors ( * persisted . UpstreamErrorsJSON )
require . NoError ( t , err )
require . Len ( t , persistedEvents , 1 )
require . Equal ( t , http . StatusTooManyRequests , persistedEvents [ 0 ]. UpstreamStatusCode )
}
2026-08-20 22:01:20 -07:00
func TestOpsErrorLoggerMiddleware_RecoveredTelemetryFiltersSkipMonitoringAttempts ( t * testing . T ) {
setupOpsErrorLogTestQueue ( t , 2 )
gin . SetMode ( gin . TestMode )
ops := service . NewOpsService ( nil , nil , nil , nil , nil , nil , nil , nil , nil , nil , nil )
router := gin . New ()
router . Use ( OpsErrorLoggerMiddleware ( ops ))
router . POST ( "/v1/responses" , func ( c * gin . Context ) {
c . Set ( service . OpsUpstreamErrorsKey , [] * service . OpsUpstreamErrorEvent {
{ UpstreamStatusCode : http . StatusTooManyRequests , Message : "visible retry" },
{ UpstreamStatusCode : http . StatusBadGateway , Message : "hidden retry" , SkipMonitoring : true },
})
c . JSON ( http . StatusOK , gin . H { "status" : "completed" })
})
recorder := httptest . NewRecorder ()
router . ServeHTTP ( recorder , httptest . NewRequest ( http . MethodPost , "/v1/responses" , nil ))
require . Equal ( t , int64 ( 1 ), OpsErrorLogQueueLength ())
job := <- opsErrorLogQueue
require . Equal ( t , "Recovered upstream error 429: visible retry" , job . entry . ErrorMessage )
require . NotNil ( t , job . entry . UpstreamErrorsJSON )
events , err := service . ParseOpsUpstreamErrors ( * job . entry . UpstreamErrorsJSON )
require . NoError ( t , err )
require . Len ( t , events , 1 )
require . Equal ( t , "visible retry" , events [ 0 ]. Message )
}
func TestOpsErrorLoggerMiddleware_RecoveredTelemetrySkipsAllHiddenAttempts ( t * testing . T ) {
setupOpsErrorLogTestQueue ( t , 2 )
gin . SetMode ( gin . TestMode )
ops := service . NewOpsService ( nil , nil , nil , nil , nil , nil , nil , nil , nil , nil , nil )
router := gin . New ()
router . Use ( OpsErrorLoggerMiddleware ( ops ))
router . POST ( "/v1/responses" , func ( c * gin . Context ) {
c . Set ( service . OpsUpstreamErrorsKey , [] * service . OpsUpstreamErrorEvent {{
UpstreamStatusCode : http . StatusTooManyRequests ,
Message : "hidden retry" ,
SkipMonitoring : true ,
}})
c . JSON ( http . StatusOK , gin . H { "status" : "completed" })
})
recorder := httptest . NewRecorder ()
router . ServeHTTP ( recorder , httptest . NewRequest ( http . MethodPost , "/v1/responses" , nil ))
require . Equal ( t , int64 ( 0 ), OpsErrorLogQueueLength ())
}
2026-08-20 21:06:48 -07:00
func TestOpsErrorLoggerMiddleware_IntermediateSkipMonitoringDoesNotHideFinalVisibleFailure ( t * testing . T ) {
setupOpsErrorLogTestQueue ( t , 2 )
gin . SetMode ( gin . TestMode )
ops := service . NewOpsService ( nil , nil , nil , nil , nil , nil , nil , nil , nil , nil , nil )
router := gin . New ()
router . Use ( OpsErrorLoggerMiddleware ( ops ))
router . POST ( "/v1/responses" , func ( c * gin . Context ) {
c . Set ( service . OpsUpstreamErrorsKey , [] * service . OpsUpstreamErrorEvent {
{ UpstreamStatusCode : http . StatusBadGateway , Message : "hidden retry" , SkipMonitoring : true },
{ UpstreamStatusCode : http . StatusServiceUnavailable , Message : "visible final" },
})
c . JSON ( http . StatusServiceUnavailable , gin . H { "error" : gin . H { "type" : "upstream_error" , "message" : "visible final" }})
})
recorder := httptest . NewRecorder ()
router . ServeHTTP ( recorder , httptest . NewRequest ( http . MethodPost , "/v1/responses" , nil ))
require . Equal ( t , int64 ( 1 ), OpsErrorLogQueueLength ())
job := <- opsErrorLogQueue
require . Equal ( t , http . StatusServiceUnavailable , job . entry . StatusCode )
require . Equal ( t , "visible final" , job . entry . ErrorMessage )
2026-08-19 09:03:37 -07:00
}
func TestOpsErrorLoggerMiddleware_CapturesSplitResponsesFailedSSE ( t * testing . T ) {
setupOpsErrorLogTestQueue ( t , 2 )
gin . SetMode ( gin . TestMode )
ops := service . NewOpsService ( nil , nil , nil , nil , nil , nil , nil , nil , nil , nil , nil )
router := gin . New ()
router . Use ( OpsErrorLoggerMiddleware ( ops ))
router . POST ( "/v1/responses" , func ( c * gin . Context ) {
setOpsRequestContext ( c , "gpt-5.5" , true )
c . Status ( http . StatusOK )
_ , _ = c . Writer . Write ([] byte ( "event: response." ))
_ , _ = c . Writer . Write ([] byte ( "failed\n" ))
_ , _ = c . Writer . Write ([] byte ( `data: {"type":"response.failed","response":{"error":{"code":"rate_limit_exceeded","message":"Too many pending requests"}}}` ))
_ , _ = c . Writer . Write ([] byte ( "\n\n" ))
})
recorder := httptest . NewRecorder ()
router . ServeHTTP ( recorder , httptest . NewRequest ( http . MethodPost , "/v1/responses" , nil ))
require . Equal ( t , http . StatusOK , recorder . Code )
require . Equal ( t , int64 ( 1 ), OpsErrorLogQueueLength ())
job := <- opsErrorLogQueue
require . Equal ( t , http . StatusTooManyRequests , job . entry . StatusCode )
require . Equal ( t , "rate_limit_error" , job . entry . ErrorType )
require . Contains ( t , job . entry . ErrorMessage , "Too many pending requests" )
}
func TestOpsCaptureWriter_CapturesSplitDataOnlyTerminalMarkers ( t * testing . T ) {
tests := [] struct {
name string
prefix string
suffix string
wantType string
wantCode string
wantError string
}{
{
name : "response failed with space" ,
prefix : `data: {"type":"response.` ,
suffix : `failed","response":{"error":{"code":"server_is_overloaded","message":"busy"}}}` ,
wantType : "overloaded_error" ,
wantCode : "server_is_overloaded" ,
wantError : "busy" ,
},
{
name : "response failed without space" ,
prefix : `data:{"type":"response.` ,
suffix : `failed","error":{"code":"rate_limit_exceeded","message":"slow down"}}` ,
wantType : "rate_limit_error" ,
wantCode : "rate_limit_exceeded" ,
wantError : "slow down" ,
},
{
name : "error with space" ,
prefix : `data: {"type":"er` ,
suffix : `ror","error":{"type":"invalid_request_error","code":"invalid_request","message":"bad input"}}` ,
wantType : "invalid_request_error" ,
wantCode : "invalid_request" ,
wantError : "bad input" ,
},
{
name : "error without space" ,
prefix : `data:{"type":"er` ,
suffix : `ror","error":{"type":"authentication_error","code":"authentication_failed","message":"sign in"}}` ,
wantType : "authentication_error" ,
wantCode : "authentication_failed" ,
wantError : "sign in" ,
},
}
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
2026-08-19 23:13:40 -07:00
state := & opsCaptureWriterState { limit : opsCaptureWriterLimit }
state . captureResponseChunk ([] byte ( tt . prefix ), http . StatusOK )
require . Empty ( t , state . buf . Bytes (), "partial frame must remain in the bounded probe" )
state . captureResponseChunk ([] byte ( tt . suffix + "\n\n" ), http . StatusOK )
2026-08-19 09:03:37 -07:00
2026-08-19 23:13:40 -07:00
parsed := parseOpsErrorResponse ( state . buf . Bytes ())
require . True ( t , state . sseCapturing )
2026-08-19 09:03:37 -07:00
require . True ( t , parsed . StreamFailure )
require . Equal ( t , tt . wantType , parsed . ErrorType )
require . Equal ( t , tt . wantCode , parsed . Code )
require . Equal ( t , tt . wantError , parsed . Message )
2026-08-19 23:13:40 -07:00
require . LessOrEqual ( t , len ( state . probe ), opsTerminalSSEFrameProbeLimit )
2026-08-19 09:03:37 -07:00
})
}
}
func TestOpsErrorLoggerMiddleware_StreamFailureUsesTerminalErrorOverAttemptContext ( t * testing . T ) {
setupOpsErrorLogTestQueue ( t , 2 )
gin . SetMode ( gin . TestMode )
ops := service . NewOpsService ( nil , nil , nil , nil , nil , nil , nil , nil , nil , nil , nil )
router := gin . New ()
router . Use ( OpsErrorLoggerMiddleware ( ops ))
router . POST ( "/v1/responses" , func ( c * gin . Context ) {
service . SetOpsUpstreamError ( c , http . StatusBadGateway , "Upstream transport error" , "earlier attempt failed" )
c . Status ( http . StatusOK )
_ , _ = c . Writer . WriteString ( "event: er" )
_ , _ = c . Writer . WriteString ( "ror\n" )
_ , _ = c . Writer . WriteString ( `data: {"type":"error","error":{"type":"invalid_request_error","code":"context_length_exceeded","message":"input exceeds the context window"}}` )
_ , _ = c . Writer . WriteString ( "\n\n" )
})
recorder := httptest . NewRecorder ()
router . ServeHTTP ( recorder , httptest . NewRequest ( http . MethodPost , "/v1/responses" , nil ))
require . Equal ( t , http . StatusOK , recorder . Code )
require . Equal ( t , int64 ( 1 ), OpsErrorLogQueueLength ())
job := <- opsErrorLogQueue
require . Equal ( t , http . StatusBadRequest , job . entry . StatusCode )
require . Equal ( t , "invalid_request_error" , job . entry . ErrorType )
require . NotNil ( t , job . entry . UpstreamStatusCode )
require . Equal ( t , http . StatusBadRequest , * job . entry . UpstreamStatusCode )
require . NotNil ( t , job . entry . UpstreamErrorMessage )
require . Equal ( t , "input exceeds the context window" , * job . entry . UpstreamErrorMessage )
}
func TestOpsErrorLoggerMiddleware_PrefersContextRequestID ( t * testing . T ) {
setupOpsErrorLogTestQueue ( t , 2 )
gin . SetMode ( gin . TestMode )
ops := service . NewOpsService ( nil , nil , nil , nil , nil , nil , nil , nil , nil , nil , nil )
router := gin . New ()
router . Use ( OpsErrorLoggerMiddleware ( ops ))
router . POST ( "/v1/responses" , func ( c * gin . Context ) {
c . Header ( "X-Request-Id" , "response-header-id" )
c . JSON ( http . StatusBadRequest , gin . H { "error" : gin . H { "type" : "invalid_request_error" , "message" : "bad input" }})
})
recorder := httptest . NewRecorder ()
request := httptest . NewRequest ( http . MethodPost , "/v1/responses" , nil )
request = request . WithContext ( context . WithValue ( request . Context (), ctxkey . RequestID , "context-request-id" ))
router . ServeHTTP ( recorder , request )
require . Equal ( t , int64 ( 1 ), OpsErrorLogQueueLength ())
job := <- opsErrorLogQueue
require . Equal ( t , "context-request-id" , job . entry . RequestID )
}
2026-07-18 00:11:18 +08:00
func TestNormalizeOpsPersistentUserAgentBoundsAndPreservesUTF8 ( t * testing . T ) {
value := strings . Repeat ( "a" , opsErrorLogMaxUserAgentBytes - 1 ) + "你" + strings . Repeat ( "b" , 32 )
got := normalizeOpsPersistentUserAgent ( " " + value + " " )
require . LessOrEqual ( t , len ( got ), opsErrorLogMaxUserAgentBytes )
require . True ( t , utf8 . ValidString ( got ))
require . NotContains ( t , got , "b" )
}
2026-07-08 02:51:51 +00:00
// 就地(in-band) SSE 错误挂在已固化的 HTTP 200 流上:wire 状态码为 200,
// 常规 status>=400 采集路径不会触发。logOpsStreamError 必须据 MarkOpsStreamError
// 补记一条错误日志,且用 IntendedStatus(429) 分级、StatusCode 仍记 wire 的 200。
func TestLogOpsStreamError_RecordsInBandConcurrencyLimit ( t * testing . T ) {
setupOpsErrorLogTestQueue ( t , 4 )
gin . SetMode ( gin . TestMode )
rec := httptest . NewRecorder ()
c , _ := gin . CreateTestContext ( rec )
c . Request = httptest . NewRequest ( http . MethodPost , "/v1/messages" , nil )
c . Set ( opsModelKey , "test-model" )
service . MarkOpsStreamError ( c , "rate_limit_error" ,
"Concurrency limit exceeded for account, please retry later" , http . StatusTooManyRequests )
ops := service . NewOpsService ( nil , nil , nil , nil , nil , nil , nil , nil , nil , nil , nil )
logOpsStreamError ( c , ops , http . StatusOK )
require . Equal ( t , int64 ( 1 ), OpsErrorLogEnqueuedTotal ())
require . Equal ( t , int64 ( 1 ), OpsErrorLogQueueLength ())
job := <- opsErrorLogQueue
require . NotNil ( t , job . entry )
require . Equal ( t , "rate_limit_error" , job . entry . ErrorType )
require . Equal ( t , "request" , job . entry . ErrorPhase )
require . True ( t , job . entry . IsBusinessLimited )
require . True ( t , job . entry . Stream )
require . Equal ( t , http . StatusOK , job . entry . StatusCode ) // wire 状态码保持 200
require . Equal ( t , "P1" , job . entry . Severity ) // 用 IntendedStatus 429 分级
require . Equal ( t , "test-model" , job . entry . Model )
require . Equal ( t , "Concurrency limit exceeded for account, please retry later" , job . entry . ErrorMessage )
}
2026-07-18 03:24:04 +08:00
func TestLogOpsStreamError_UpstreamFailureCountsTowardsSLA ( t * testing . T ) {
setupOpsErrorLogTestQueue ( t , 4 )
gin . SetMode ( gin . TestMode )
rec := httptest . NewRecorder ()
c , _ := gin . CreateTestContext ( rec )
c . Request = httptest . NewRequest ( http . MethodPost , "/v1/chat/completions" , nil )
c . Set ( opsModelKey , "gpt-5.6-sol" )
service . MarkOpsStreamFailure (
c ,
"upstream_error" ,
service . OpenAIUpstreamHTTP2StreamErrorCode ,
"Upstream HTTP/2 stream failed" ,
http . StatusBadGateway ,
)
ops := service . NewOpsService ( nil , nil , nil , nil , nil , nil , nil , nil , nil , nil , nil )
logOpsStreamError ( c , ops , http . StatusOK )
job := <- opsErrorLogQueue
require . NotNil ( t , job . entry )
require . Equal ( t , http . StatusBadGateway , job . entry . StatusCode )
require . Equal ( t , "upstream_error" , job . entry . ErrorType )
require . Equal ( t , "upstream" , job . entry . ErrorPhase )
require . Equal ( t , "provider" , job . entry . ErrorOwner )
require . False ( t , job . entry . IsBusinessLimited )
require . Contains ( t , job . entry . ErrorBody , service . OpenAIUpstreamHTTP2StreamErrorCode )
}
2026-07-08 02:51:51 +00:00
// 未标记流内错误时 logOpsStreamError 必须是 no-op(不误记正常的 200 流)。
func TestLogOpsStreamError_NoopWhenNotMarked ( t * testing . T ) {
setupOpsErrorLogTestQueue ( t , 4 )
gin . SetMode ( gin . TestMode )
rec := httptest . NewRecorder ()
c , _ := gin . CreateTestContext ( rec )
c . Request = httptest . NewRequest ( http . MethodPost , "/v1/messages" , nil )
ops := service . NewOpsService ( nil , nil , nil , nil , nil , nil , nil , nil , nil , nil , nil )
logOpsStreamError ( c , ops , http . StatusOK )
require . Equal ( t , int64 ( 0 ), OpsErrorLogEnqueuedTotal ())
}
// 命中 skip_monitoring=true 透传规则时不落库,与其它采集分支一致。
func TestLogOpsStreamError_SkipWhenPassthroughSkipMonitoring ( t * testing . T ) {
setupOpsErrorLogTestQueue ( t , 4 )
gin . SetMode ( gin . TestMode )
rec := httptest . NewRecorder ()
c , _ := gin . CreateTestContext ( rec )
c . Request = httptest . NewRequest ( http . MethodPost , "/v1/messages" , nil )
service . MarkOpsStreamError ( c , "upstream_error" , "Upstream request failed" , http . StatusBadGateway )
c . Set ( service . OpsSkipPassthroughKey , true )
ops := service . NewOpsService ( nil , nil , nil , nil , nil , nil , nil , nil , nil , nil , nil )
logOpsStreamError ( c , ops , http . StatusOK )
require . Equal ( t , int64 ( 0 ), OpsErrorLogEnqueuedTotal ())
}
2026-08-19 23:13:40 -07:00
func TestShouldSkipFinalOpsFailureUsesOnlyFinalAttemptRule ( t * testing . T ) {
gin . SetMode ( gin . TestMode )
c , _ := gin . CreateTestContext ( httptest . NewRecorder ())
c . Set ( service . OpsUpstreamErrorsKey , [] * service . OpsUpstreamErrorEvent {
{ UpstreamStatusCode : http . StatusBadGateway , Message : "hidden intermediate" , SkipMonitoring : true },
{ UpstreamStatusCode : http . StatusServiceUnavailable , Message : "visible final" },
})
require . False ( t , shouldSkipFinalOpsFailure ( c ))
c . Set ( service . OpsUpstreamErrorsKey , [] * service . OpsUpstreamErrorEvent {
{ UpstreamStatusCode : http . StatusBadGateway , Message : "visible intermediate" },
nil ,
{ UpstreamStatusCode : http . StatusServiceUnavailable , Message : "hidden final" , SkipMonitoring : true },
})
require . True ( t , shouldSkipFinalOpsFailure ( c ))
}
2026-07-08 02:51:51 +00:00
// MarkOpsStreamError 采用「首个标记生效」:后续的通用兜底帧不得覆盖根因错误。
func TestMarkOpsStreamError_FirstWins ( t * testing . T ) {
gin . SetMode ( gin . TestMode )
rec := httptest . NewRecorder ()
c , _ := gin . CreateTestContext ( rec )
service . MarkOpsStreamError ( c , "rate_limit_error" , "Concurrency limit exceeded for account" , http . StatusTooManyRequests )
service . MarkOpsStreamError ( c , "upstream_error" , "Upstream request failed" , http . StatusBadGateway )
se , ok := service . GetOpsStreamError ( c )
require . True ( t , ok )
require . Equal ( t , "rate_limit_error" , se . ErrType )
require . Equal ( t , "Concurrency limit exceeded for account" , se . Message )
require . Equal ( t , http . StatusTooManyRequests , se . IntendedStatus )
}
2026-08-19 23:13:40 -07:00
func TestLogOpsStreamError_RecordsOneFailurePerWebSocketTurn ( t * testing . T ) {
setupOpsErrorLogTestQueue ( t , 4 )
gin . SetMode ( gin . TestMode )
c , _ := gin . CreateTestContext ( httptest . NewRecorder ())
c . Request = httptest . NewRequest ( http . MethodGet , "/v1/responses" , nil )
service . SetOpenAIClientTransport ( c , service . OpenAIClientTransportWS )
service . BeginOpsStreamTurn ( c , 1 )
service . MarkOpsStreamFailure ( c , "rate_limit_error" , "rate_limit_exceeded" , "turn one failed" , http . StatusTooManyRequests )
service . MarkOpsStreamError ( c , "upstream_error" , "generic duplicate for turn one" , http . StatusBadGateway )
service . BeginOpsStreamTurn ( c , 2 )
service . MarkOpsStreamFailure ( c , "permission_error" , "permission_denied" , "turn two failed" , http . StatusForbidden )
streamErrors := service . GetOpsStreamErrors ( c )
require . Len ( t , streamErrors , 2 )
require . Equal ( t , 1 , streamErrors [ 0 ]. Turn )
require . Equal ( t , 2 , streamErrors [ 1 ]. Turn )
ops := service . NewOpsService ( nil , nil , nil , nil , nil , nil , nil , nil , nil , nil , nil )
logOpsStreamError ( c , ops , http . StatusSwitchingProtocols )
require . Equal ( t , int64 ( 2 ), OpsErrorLogQueueLength ())
first := <- opsErrorLogQueue
second := <- opsErrorLogQueue
require . Equal ( t , "turn one failed" , first . entry . ErrorMessage )
require . Equal ( t , http . StatusTooManyRequests , first . entry . StatusCode )
require . Equal ( t , "turn two failed" , second . entry . ErrorMessage )
require . Equal ( t , http . StatusForbidden , second . entry . StatusCode )
}
2026-02-28 10:37:06 +08:00
func TestIsKnownOpsErrorType ( t * testing . T ) {
known := [] string {
"invalid_request_error" ,
"authentication_error" ,
"rate_limit_error" ,
"billing_error" ,
"subscription_error" ,
"upstream_error" ,
"overloaded_error" ,
"api_error" ,
"not_found_error" ,
"forbidden_error" ,
}
for _ , k := range known {
require . True ( t , isKnownOpsErrorType ( k ), "expected known: %s" , k )
}
unknown := [] string { "<nil>" , "null" , "" , "random_error" , "some_new_type" , "<nil>\u003e" }
for _ , u := range unknown {
require . False ( t , isKnownOpsErrorType ( u ), "expected unknown: %q" , u )
}
}
func TestNormalizeOpsErrorType ( t * testing . T ) {
tests := [] struct {
name string
errType string
code string
want string
}{
// Known types pass through.
{ "known invalid_request_error" , "invalid_request_error" , "" , "invalid_request_error" },
{ "known rate_limit_error" , "rate_limit_error" , "" , "rate_limit_error" },
{ "known upstream_error" , "upstream_error" , "" , "upstream_error" },
// Unknown/garbage types are rejected and fall through to code-based or default.
{ "nil literal from upstream" , "<nil>" , "" , "api_error" },
{ "null string" , "null" , "" , "api_error" },
{ "random string" , "something_weird" , "" , "api_error" },
// Unknown type but known code still maps correctly.
{ "nil with INSUFFICIENT_BALANCE code" , "<nil>" , "INSUFFICIENT_BALANCE" , "billing_error" },
{ "nil with USAGE_LIMIT_EXCEEDED code" , "<nil>" , "USAGE_LIMIT_EXCEEDED" , "subscription_error" },
// Empty type falls through to code-based mapping.
{ "empty type with balance code" , "" , "INSUFFICIENT_BALANCE" , "billing_error" },
{ "empty type with subscription code" , "" , "SUBSCRIPTION_NOT_FOUND" , "subscription_error" },
{ "empty type no code" , "" , "" , "api_error" },
2026-02-28 14:55:07 +08:00
// Known type overrides conflicting code-based mapping.
{ "known type overrides conflicting code" , "rate_limit_error" , "INSUFFICIENT_BALANCE" , "rate_limit_error" },
2026-02-28 10:37:06 +08:00
}
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
got := normalizeOpsErrorType ( tt . errType , tt . code )
require . Equal ( t , tt . want , got )
})
}
}
2026-03-21 23:49:50 +08:00
2026-05-18 16:52:06 +08:00
func TestClassifyOpsNoAvailableAccountsExcludedFromSLA ( t * testing . T ) {
const message = "No available accounts"
gin . SetMode ( gin . TestMode )
rec := httptest . NewRecorder ()
c , _ := gin . CreateTestContext ( rec )
markOpsRoutingCapacityLimited ( c )
errType := normalizeOpsErrorType ( "api_error" , "" )
phase , isBusinessLimited , errorOwner , errorSource := classifyOpsErrorLog ( c , errType , message , "" , http . StatusServiceUnavailable )
require . Equal ( t , "api_error" , errType )
require . Equal ( t , "routing" , phase )
require . True ( t , isBusinessLimited )
require . Equal ( t , "platform" , errorOwner )
require . Equal ( t , "gateway" , errorSource )
}
func TestClassifyOpsRoutingCapacityMarkerExcludesMaskedSelectionFailureFromSLA ( t * testing . T ) {
gin . SetMode ( gin . TestMode )
rec := httptest . NewRecorder ()
c , _ := gin . CreateTestContext ( rec )
markOpsRoutingCapacityLimited ( c )
phase , isBusinessLimited , errorOwner , errorSource := classifyOpsErrorLog (
c ,
"api_error" ,
"Service temporarily unavailable" ,
"" ,
http . StatusServiceUnavailable ,
)
require . Equal ( t , "routing" , phase )
require . True ( t , isBusinessLimited )
require . Equal ( t , "platform" , errorOwner )
require . Equal ( t , "gateway" , errorSource )
}
2026-08-20 10:52:29 +08:00
func TestClassifyOpsLocalModelConfigurationRejection ( t * testing . T ) {
gin . SetMode ( gin . TestMode )
rec := httptest . NewRecorder ()
c , _ := gin . CreateTestContext ( rec )
service . MarkOpsClientBusinessLimited ( c , service . OpsClientBusinessLimitedReasonLocalModelConfiguration )
phase , isBusinessLimited , errorOwner , errorSource := classifyOpsErrorLog (
c ,
"model_not_found" ,
"Model \"gpt-missing\" is not supported by any configured account in this group" ,
"" ,
http . StatusNotFound ,
)
require . Equal ( t , "routing" , phase )
require . True ( t , isBusinessLimited )
require . Equal ( t , "platform" , errorOwner )
require . Equal ( t , "gateway" , errorSource )
}
func TestClassifyOpsLocalModelConfigurationOverridesStaleUpstreamMarkers ( t * testing . T ) {
gin . SetMode ( gin . TestMode )
c , _ := gin . CreateTestContext ( httptest . NewRecorder ())
service . MarkOpsClientBusinessLimited ( c , service . OpsClientBusinessLimitedReasonLocalModelConfiguration )
c . Set ( service . OpsUpstreamStatusCodeKey , http . StatusUnauthorized )
c . Set ( service . OpsUpstreamErrorsKey , [] * service . OpsUpstreamErrorEvent {{
Stage : string ( service . GatewayFailureStageAccountAuth ),
UpstreamStatusCode : http . StatusUnauthorized ,
}})
phase , limited , owner , source := classifyOpsErrorLog ( c , "model_not_found" , "unsupported configured model" , "" , http . StatusNotFound )
require . Equal ( t , "routing" , phase )
require . True ( t , limited )
require . Equal ( t , "platform" , owner )
require . Equal ( t , "gateway" , source )
}
func TestClassifyOpsLocalModelConfigurationRequiresMarkerAndReason ( t * testing . T ) {
gin . SetMode ( gin . TestMode )
c , _ := gin . CreateTestContext ( httptest . NewRecorder ())
c . Set ( service . OpsClientBusinessLimitedReasonKey , service . OpsClientBusinessLimitedReasonLocalModelConfiguration )
c . Set ( service . OpsUpstreamStatusCodeKey , http . StatusBadGateway )
phase , limited , owner , source := classifyOpsErrorLog ( c , "upstream_error" , "provider failed" , "" , http . StatusBadGateway )
require . Equal ( t , "upstream" , phase )
require . False ( t , limited )
require . Equal ( t , "provider" , owner )
require . Equal ( t , "upstream_http" , source )
}
func TestOpsErrorLoggerMiddleware_LocalModelConfigurationFields ( t * testing . T ) {
setupOpsErrorLogTestQueue ( t , 1 )
gin . SetMode ( gin . TestMode )
ops := service . NewOpsService ( nil , nil , nil , nil , nil , nil , nil , nil , nil , nil , nil )
router := gin . New ()
router . Use ( OpsErrorLoggerMiddleware ( ops ))
router . POST ( "/v1/chat/completions" , func ( c * gin . Context ) {
service . MarkOpsClientBusinessLimited ( c , service . OpsClientBusinessLimitedReasonLocalModelConfiguration )
c . Set ( opsAccountIDKey , int64 ( 99 ))
c . Set ( opsUpstreamModelKey , "stale-upstream-model" )
setActualUpstreamEndpoint ( c , "/v1/chat/completions" )
c . Set ( service . OpsUpstreamStatusCodeKey , http . StatusUnauthorized )
c . Set ( service . OpsUpstreamErrorMessageKey , "stale upstream error" )
c . Set ( service . OpsUpstreamErrorDetailKey , "stale upstream detail" )
c . Set ( service . OpsUpstreamErrorsKey , [] * service . OpsUpstreamErrorEvent {{
Stage : string ( service . GatewayFailureStageAccountAuth ),
UpstreamStatusCode : http . StatusUnauthorized ,
Message : "stale auth failure" ,
}})
c . JSON ( http . StatusNotFound , gin . H {
"error" : gin . H {
"type" : "model_not_found" ,
"message" : "Model \"gpt-missing\" is not supported by any configured account in this group" ,
},
})
})
w := httptest . NewRecorder ()
req := httptest . NewRequest ( http . MethodPost , "/v1/chat/completions" , nil )
router . ServeHTTP ( w , req )
require . Equal ( t , http . StatusNotFound , w . Code )
require . JSONEq ( t , `{"error":{"type":"model_not_found","message":"Model \"gpt-missing\" is not supported by any configured account in this group"}}` , w . Body . String ())
job := <- opsErrorLogQueue
require . Equal ( t , http . StatusNotFound , job . entry . StatusCode )
require . Equal ( t , "routing" , job . entry . ErrorPhase )
require . True ( t , job . entry . IsBusinessLimited )
require . Equal ( t , "platform" , job . entry . ErrorOwner )
require . Equal ( t , "gateway" , job . entry . ErrorSource )
require . Nil ( t , job . entry . AccountID )
require . Nil ( t , job . entry . UpstreamStatusCode )
require . Nil ( t , job . entry . UpstreamErrors )
require . Nil ( t , job . entry . UpstreamErrorMessage )
require . Nil ( t , job . entry . UpstreamErrorDetail )
require . Empty ( t , job . entry . UpstreamModel )
require . Empty ( t , job . entry . UpstreamEndpoint )
}
2026-05-18 16:52:06 +08:00
func TestClassifyOpsAuthClientErrorsExcludedFromSLA ( t * testing . T ) {
tests := [] struct {
name string
errType string
message string
code string
status int
}{
{
name : "standard invalid API key" ,
errType : "api_error" ,
message : "Invalid API key" ,
code : "INVALID_API_KEY" ,
status : http . StatusUnauthorized ,
},
{
name : "standard missing API key" ,
errType : "api_error" ,
message : "API key is required in Authorization header (Bearer scheme), x-api-key header, or x-goog-api-key header" ,
code : "API_KEY_REQUIRED" ,
status : http . StatusUnauthorized ,
},
2026-05-20 22:01:33 +08:00
{
name : "expired local API key" ,
errType : "api_error" ,
message : "API key 已过期" ,
code : "API_KEY_EXPIRED" ,
status : http . StatusForbidden ,
},
{
name : "disabled local API key" ,
errType : "api_error" ,
message : "API key is disabled" ,
code : "API_KEY_DISABLED" ,
status : http . StatusUnauthorized ,
},
{
name : "local API key user missing" ,
errType : "api_error" ,
message : "User associated with API key not found" ,
code : "USER_NOT_FOUND" ,
status : http . StatusUnauthorized ,
},
{
name : "inactive local API key user" ,
errType : "api_error" ,
message : "User account is not active" ,
code : "USER_INACTIVE" ,
status : http . StatusUnauthorized ,
},
2026-05-26 17:19:09 +08:00
{
name : "deleted local API key group" ,
errType : "api_error" ,
message : "API Key 所属分组已删除" ,
code : "GROUP_DELETED" ,
status : http . StatusForbidden ,
},
{
name : "disabled local API key group" ,
errType : "api_error" ,
message : "API Key 所属分组已停用" ,
code : "GROUP_DISABLED" ,
status : http . StatusForbidden ,
},
{
name : "google deleted API key group message without semantic code" ,
errType : "api_error" ,
message : "API Key 所属分组已删除" ,
code : "403" ,
status : http . StatusForbidden ,
},
{
name : "anthropic unassigned API key group" ,
errType : "permission_error" ,
message : "API Key is not assigned to any group and cannot be used. Please contact the administrator to assign it to a group." ,
code : "" ,
status : http . StatusForbidden ,
},
2026-05-18 16:52:06 +08:00
{
name : "google invalid API key" ,
errType : "api_error" ,
message : "Invalid API key" ,
code : "401" ,
status : http . StatusUnauthorized ,
},
{
name : "google missing API key" ,
errType : "api_error" ,
message : "API key is required" ,
code : "401" ,
status : http . StatusUnauthorized ,
},
2026-05-20 22:01:33 +08:00
{
name : "google disabled API key" ,
errType : "api_error" ,
message : "API key is disabled" ,
code : "401" ,
status : http . StatusUnauthorized ,
},
{
name : "google local API key user missing" ,
errType : "api_error" ,
message : "User associated with API key not found" ,
code : "401" ,
status : http . StatusUnauthorized ,
},
{
name : "google inactive local API key user" ,
errType : "api_error" ,
message : "User account is not active" ,
code : "401" ,
status : http . StatusUnauthorized ,
},
2026-05-18 16:52:06 +08:00
}
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
gin . SetMode ( gin . TestMode )
rec := httptest . NewRecorder ()
c , _ := gin . CreateTestContext ( rec )
errType := normalizeOpsErrorType ( tt . errType , tt . code )
phase , isBusinessLimited , errorOwner , errorSource := classifyOpsErrorLog ( c , errType , tt . message , tt . code , tt . status )
2026-08-19 23:13:40 -07:00
wantErrType := "api_error"
if tt . errType == "permission_error" {
wantErrType = "permission_error"
}
require . Equal ( t , wantErrType , errType )
2026-05-18 16:52:06 +08:00
require . Equal ( t , "auth" , phase )
require . True ( t , isBusinessLimited )
require . Equal ( t , "client" , errorOwner )
require . Equal ( t , "client_request" , errorSource )
})
}
}
2026-05-20 22:01:33 +08:00
func TestClassifyOpsLocalBusinessLimitErrorsExcludedFromSLA ( t * testing . T ) {
tests := [] struct {
name string
errType string
message string
code string
status int
wantErrType string
wantPhase string
}{
{
name : "standard API key quota exhausted" ,
errType : "api_error" ,
message : "API key 额度已用完" ,
code : "API_KEY_QUOTA_EXHAUSTED" ,
status : http . StatusTooManyRequests ,
wantErrType : "api_error" ,
wantPhase : "request" ,
},
{
name : "standard query API key deprecated" ,
errType : "api_error" ,
message : "API key in query parameter is deprecated. Please use Authorization header instead." ,
code : "api_key_in_query_deprecated" ,
status : http . StatusBadRequest ,
wantErrType : "api_error" ,
wantPhase : "request" ,
},
{
name : "google query API key deprecated" ,
errType : "api_error" ,
message : "Query parameter api_key is deprecated. Use Authorization header or key instead." ,
code : "400" ,
status : http . StatusBadRequest ,
wantErrType : "api_error" ,
wantPhase : "request" ,
},
{
name : "google no active subscription" ,
errType : "api_error" ,
message : "No active subscription found for this group" ,
code : "403" ,
status : http . StatusForbidden ,
wantErrType : "api_error" ,
wantPhase : "request" ,
},
2026-05-26 17:19:09 +08:00
{
name : "gateway subscription invalid cache recheck" ,
errType : "billing_error" ,
message : "subscription is invalid or expired" ,
code : "billing_error" ,
status : http . StatusForbidden ,
wantErrType : "billing_error" ,
wantPhase : "request" ,
},
2026-05-20 22:01:33 +08:00
{
name : "google insufficient account balance" ,
errType : "api_error" ,
message : "Insufficient account balance" ,
code : "403" ,
status : http . StatusForbidden ,
wantErrType : "api_error" ,
wantPhase : "request" ,
},
{
name : "gateway billing cache insufficient balance" ,
errType : "billing_error" ,
message : "insufficient balance" ,
code : "" ,
status : http . StatusForbidden ,
wantErrType : "billing_error" ,
wantPhase : "request" ,
},
{
name : "gemini group platform mismatch" ,
errType : "api_error" ,
message : "API key group platform is not gemini" ,
code : "400" ,
status : http . StatusBadRequest ,
wantErrType : "api_error" ,
wantPhase : "request" ,
},
{
name : "gateway API key 5h rate limit" ,
errType : "api_error" ,
message : "api key 5小时限额已用完" ,
code : "rate_limit_exceeded" ,
status : http . StatusTooManyRequests ,
wantErrType : "api_error" ,
wantPhase : "request" ,
},
{
name : "gateway group RPM limit" ,
errType : "api_error" ,
message : "group requests-per-minute limit exceeded" ,
code : "rate_limit_exceeded" ,
status : http . StatusTooManyRequests ,
wantErrType : "api_error" ,
wantPhase : "request" ,
},
{
name : "google subscription daily limit" ,
errType : "api_error" ,
message : "daily usage limit exceeded" ,
code : "429" ,
status : http . StatusTooManyRequests ,
wantErrType : "api_error" ,
wantPhase : "request" ,
},
2026-05-26 17:19:09 +08:00
{
name : "user platform daily quota exhausted" ,
errType : "api_error" ,
message : "Daily usage quota exhausted for this platform." ,
code : "rate_limit_exceeded" ,
status : http . StatusTooManyRequests ,
wantErrType : "api_error" ,
wantPhase : "request" ,
},
{
name : "local pending queue limit" ,
errType : "rate_limit_error" ,
message : "Too many pending requests, please retry later" ,
code : "" ,
status : http . StatusTooManyRequests ,
wantErrType : "rate_limit_error" ,
wantPhase : "request" ,
},
{
name : "local concurrency limit" ,
errType : "rate_limit_error" ,
message : "Concurrency limit exceeded for user, please retry later" ,
code : "" ,
status : http . StatusTooManyRequests ,
wantErrType : "rate_limit_error" ,
wantPhase : "request" ,
},
{
name : "group claude code only feature gate" ,
errType : "permission_error" ,
message : "This group is restricted to Claude Code clients (/v1/messages only)" ,
code : "" ,
status : http . StatusForbidden ,
2026-08-19 23:13:40 -07:00
wantErrType : "permission_error" ,
2026-05-26 17:19:09 +08:00
wantPhase : "request" ,
},
{
name : "group image generation feature gate" ,
errType : "permission_error" ,
message : "Image generation is not enabled for this group" ,
code : "" ,
status : http . StatusForbidden ,
2026-08-19 23:13:40 -07:00
wantErrType : "permission_error" ,
2026-05-26 17:19:09 +08:00
wantPhase : "request" ,
},
2026-09-01 10:15:45 +08:00
{
name : "group reasoning effort over limit deny" ,
errType : "permission_error" ,
message : `reasoning effort "high" exceeds this group's limit of "low"` ,
code : "" ,
status : http . StatusForbidden ,
wantErrType : "permission_error" ,
wantPhase : "request" ,
},
2026-05-26 17:19:09 +08:00
{
name : "route token counting platform unsupported" ,
errType : "not_found_error" ,
message : "Token counting is not supported for this platform" ,
code : "" ,
status : http . StatusNotFound ,
wantErrType : "not_found_error" ,
wantPhase : "request" ,
},
{
name : "route images API platform unsupported" ,
errType : "not_found_error" ,
message : "Images API is not supported for this platform" ,
code : "" ,
status : http . StatusNotFound ,
wantErrType : "not_found_error" ,
wantPhase : "request" ,
},
{
name : "antigravity model whitelist feature gate" ,
errType : "permission_error" ,
message : "model claude-3-5-sonnet not in whitelist" ,
code : "" ,
status : http . StatusForbidden ,
2026-08-19 23:13:40 -07:00
wantErrType : "permission_error" ,
2026-05-26 17:19:09 +08:00
wantPhase : "request" ,
},
{
name : "google antigravity model whitelist feature gate" ,
errType : "api_error" ,
message : "model gemini-2.5-pro not in whitelist" ,
code : "403" ,
status : http . StatusForbidden ,
wantErrType : "api_error" ,
wantPhase : "request" ,
},
{
name : "claude beta policy block" ,
errType : "invalid_request_error" ,
message : "beta feature interleaved-thinking-2025-05-14 is not allowed" ,
code : "" ,
status : http . StatusBadRequest ,
wantErrType : "invalid_request_error" ,
wantPhase : "request" ,
},
{
name : "openai fast policy block" ,
errType : "permission_error" ,
message : "openai service_tier=priority is not allowed for model gpt-5.5" ,
code : "" ,
status : http . StatusForbidden ,
2026-08-19 23:13:40 -07:00
wantErrType : "permission_error" ,
2026-05-26 17:19:09 +08:00
wantPhase : "request" ,
},
{
name : "codex official client policy block" ,
errType : "forbidden_error" ,
message : "This account only allows Codex official clients" ,
code : "" ,
status : http . StatusForbidden ,
wantErrType : "forbidden_error" ,
wantPhase : "request" ,
},
{
name : "openai wsv1 unsupported feature gate" ,
errType : "invalid_request_error" ,
message : "OpenAI WSv1 is temporarily unsupported. Please enable responses_websockets_v2." ,
code : "" ,
status : http . StatusBadRequest ,
wantErrType : "invalid_request_error" ,
wantPhase : "request" ,
},
{
name : "openai passthrough instructions policy block" ,
errType : "forbidden_error" ,
message : "OpenAI codex passthrough requires a non-empty instructions field" ,
code : "" ,
status : http . StatusForbidden ,
wantErrType : "forbidden_error" ,
wantPhase : "request" ,
},
2026-05-20 22:01:33 +08:00
}
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
gin . SetMode ( gin . TestMode )
rec := httptest . NewRecorder ()
c , _ := gin . CreateTestContext ( rec )
errType := normalizeOpsErrorType ( tt . errType , tt . code )
phase , isBusinessLimited , errorOwner , errorSource := classifyOpsErrorLog ( c , errType , tt . message , tt . code , tt . status )
require . Equal ( t , tt . wantErrType , errType )
require . Equal ( t , tt . wantPhase , phase )
require . True ( t , isBusinessLimited )
require . Equal ( t , "client" , errorOwner )
require . Equal ( t , "client_request" , errorSource )
})
}
}
2026-05-19 15:41:54 +08:00
func TestClassifyOpsIPRestrictionAccessDeniedExcludedFromSLA ( t * testing . T ) {
gin . SetMode ( gin . TestMode )
rec := httptest . NewRecorder ()
c , _ := gin . CreateTestContext ( rec )
service . MarkOpsClientBusinessLimited ( c , service . OpsClientBusinessLimitedReasonIPRestriction )
errType := normalizeOpsErrorType ( "api_error" , "ACCESS_DENIED" )
phase , isBusinessLimited , errorOwner , errorSource := classifyOpsErrorLog ( c , errType , "Access denied" , "ACCESS_DENIED" , http . StatusForbidden )
require . Equal ( t , "api_error" , errType )
require . Equal ( t , "auth" , phase )
require . True ( t , isBusinessLimited )
require . Equal ( t , "client" , errorOwner )
require . Equal ( t , "client_request" , errorSource )
}
2026-05-26 17:19:09 +08:00
func TestClassifyOpsClientBusinessLimitedMarkerExcludesCustomPolicyDenialFromSLA ( t * testing . T ) {
gin . SetMode ( gin . TestMode )
rec := httptest . NewRecorder ()
c , _ := gin . CreateTestContext ( rec )
service . MarkOpsClientBusinessLimited ( c , service . OpsClientBusinessLimitedReasonLocalPolicyDenied )
errType := normalizeOpsErrorType ( "invalid_request_error" , "" )
phase , isBusinessLimited , errorOwner , errorSource := classifyOpsErrorLog ( c , errType , "custom admin policy message" , "" , http . StatusBadRequest )
require . Equal ( t , "invalid_request_error" , errType )
require . Equal ( t , "auth" , phase )
require . True ( t , isBusinessLimited )
require . Equal ( t , "client" , errorOwner )
require . Equal ( t , "client_request" , errorSource )
}
2026-05-19 15:41:54 +08:00
func TestClassifyOpsOtherErrorsStillCountForSLA ( t * testing . T ) {
gin . SetMode ( gin . TestMode )
rec := httptest . NewRecorder ()
c , _ := gin . CreateTestContext ( rec )
errType := normalizeOpsErrorType ( "api_error" , "INTERNAL_ERROR" )
phase , isBusinessLimited , errorOwner , errorSource := classifyOpsErrorLog ( c , errType , "Failed to validate API key" , "INTERNAL_ERROR" , http . StatusInternalServerError )
require . Equal ( t , "api_error" , errType )
require . Equal ( t , "internal" , phase )
require . False ( t , isBusinessLimited )
require . Equal ( t , "platform" , errorOwner )
require . Equal ( t , "gateway" , errorSource )
}
2026-05-18 16:52:06 +08:00
func TestClassifyOpsUnsupportedModelExcludedFromSLA ( t * testing . T ) {
tests := [] string {
"No available accounts: no available accounts supporting model: made-up-model" ,
"No available accounts: no available OpenAI accounts supporting model: made-up-model" ,
"No available Gemini accounts: no available Gemini accounts supporting model: made-up-model" ,
"No available accounts: no available accounts supporting model: made-up-model (channel pricing restriction)" ,
}
for _ , message := range tests {
t . Run ( message , func ( t * testing . T ) {
gin . SetMode ( gin . TestMode )
rec := httptest . NewRecorder ()
c , _ := gin . CreateTestContext ( rec )
markOpsRoutingCapacityLimited ( c )
errType := normalizeOpsErrorType ( "api_error" , "" )
phase , isBusinessLimited , errorOwner , errorSource := classifyOpsErrorLog ( c , errType , message , "" , http . StatusServiceUnavailable )
require . Equal ( t , "api_error" , errType )
require . Equal ( t , "routing" , phase )
require . True ( t , isBusinessLimited )
require . Equal ( t , "platform" , errorOwner )
require . Equal ( t , "gateway" , errorSource )
})
}
}
func TestClassifyOpsUnmarkedNoAvailableTextStillCountsForSLA ( t * testing . T ) {
gin . SetMode ( gin . TestMode )
rec := httptest . NewRecorder ()
c , _ := gin . CreateTestContext ( rec )
phase , isBusinessLimited , errorOwner , errorSource := classifyOpsErrorLog (
c ,
"api_error" ,
"No available accounts" ,
"" ,
http . StatusServiceUnavailable ,
)
require . Equal ( t , "routing" , phase )
require . False ( t , isBusinessLimited )
require . Equal ( t , "platform" , errorOwner )
require . Equal ( t , "gateway" , errorSource )
}
func TestClassifyOpsUpstreamAuthTextStillCountsForSLA ( t * testing . T ) {
2026-05-20 22:01:33 +08:00
tests := [] struct {
name string
message string
code string
status int
}{
{
name : "invalid API key" ,
message : "Invalid API key" ,
code : "401" ,
status : http . StatusUnauthorized ,
},
{
name : "disabled API key" ,
message : "API key is disabled" ,
code : "API_KEY_DISABLED" ,
status : http . StatusUnauthorized ,
},
{
name : "gemini group platform mismatch" ,
message : "API key group platform is not gemini" ,
code : "400" ,
status : http . StatusBadRequest ,
},
{
name : "provider balance error" ,
message : "Insufficient account balance" ,
code : "INSUFFICIENT_BALANCE" ,
status : http . StatusForbidden ,
},
{
name : "provider subscription error" ,
message : "No active subscription found for this group" ,
code : "SUBSCRIPTION_NOT_FOUND" ,
status : http . StatusForbidden ,
},
{
name : "provider quota error" ,
message : "api key 额度已用完" ,
code : "API_KEY_QUOTA_EXHAUSTED" ,
status : http . StatusTooManyRequests ,
},
2026-05-26 17:19:09 +08:00
{
name : "provider deleted group shaped error" ,
message : "API Key 所属分组已删除" ,
code : "GROUP_DELETED" ,
status : http . StatusForbidden ,
},
{
name : "provider unassigned group shaped error" ,
message : "API Key is not assigned to any group and cannot be used. Please contact the administrator to assign it to a group." ,
code : "403" ,
status : http . StatusForbidden ,
},
{
name : "provider local quota shaped error" ,
message : "Daily usage quota exhausted for this platform." ,
code : "rate_limit_exceeded" ,
status : http . StatusTooManyRequests ,
},
{
name : "provider feature gate shaped error" ,
message : "Image generation is not enabled for this group" ,
code : "403" ,
status : http . StatusForbidden ,
},
2026-09-01 10:15:45 +08:00
{
name : "provider reasoning effort over limit shaped error" ,
message : `reasoning effort "high" exceeds this group's limit of "low"` ,
code : "403" ,
status : http . StatusForbidden ,
},
2026-05-26 17:19:09 +08:00
{
name : "provider token counting unsupported shaped error" ,
message : "Token counting is not supported for this platform" ,
code : "404" ,
status : http . StatusNotFound ,
},
{
name : "provider image API unsupported shaped error" ,
message : "Images API is not supported for this platform" ,
code : "404" ,
status : http . StatusNotFound ,
},
{
name : "provider antigravity whitelist shaped error" ,
message : "model claude-3-5-sonnet not in whitelist" ,
code : "403" ,
status : http . StatusForbidden ,
},
{
name : "provider beta policy shaped error" ,
message : "beta feature interleaved-thinking-2025-05-14 is not allowed" ,
code : "400" ,
status : http . StatusBadRequest ,
},
{
name : "provider openai fast policy shaped error" ,
message : "openai service_tier=priority is not allowed for model gpt-5.5" ,
code : "403" ,
status : http . StatusForbidden ,
},
{
name : "provider codex client policy shaped error" ,
message : "This account only allows Codex official clients" ,
code : "403" ,
status : http . StatusForbidden ,
},
{
name : "provider wsv1 unsupported shaped error" ,
message : "OpenAI WSv1 is temporarily unsupported. Please enable responses_websockets_v2." ,
code : "400" ,
status : http . StatusBadRequest ,
},
{
name : "provider passthrough instructions shaped error" ,
message : "OpenAI codex passthrough requires a non-empty instructions field" ,
code : "403" ,
status : http . StatusForbidden ,
},
2026-05-20 22:01:33 +08:00
}
2026-05-18 16:52:06 +08:00
2026-05-20 22:01:33 +08:00
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
gin . SetMode ( gin . TestMode )
rec := httptest . NewRecorder ()
c , _ := gin . CreateTestContext ( rec )
service . SetOpsUpstreamError ( c , tt . status , tt . message , "" )
2026-05-18 16:52:06 +08:00
2026-05-20 22:01:33 +08:00
phase , isBusinessLimited , errorOwner , errorSource := classifyOpsErrorLog (
c ,
"api_error" ,
tt . message ,
tt . code ,
tt . status ,
)
require . Equal ( t , "upstream" , phase )
require . False ( t , isBusinessLimited )
require . Equal ( t , "provider" , errorOwner )
require . Equal ( t , "upstream_http" , errorSource )
})
}
2026-05-18 16:52:06 +08:00
}
func TestClassifyOpsUpstreamNoAvailableTextStillCountsForSLA ( t * testing . T ) {
gin . SetMode ( gin . TestMode )
rec := httptest . NewRecorder ()
c , _ := gin . CreateTestContext ( rec )
service . SetOpsUpstreamError ( c , http . StatusServiceUnavailable , "No available accounts" , "" )
phase , isBusinessLimited , errorOwner , errorSource := classifyOpsErrorLog (
c ,
"api_error" ,
"No available accounts" ,
"" ,
http . StatusServiceUnavailable ,
)
require . Equal ( t , "upstream" , phase )
require . False ( t , isBusinessLimited )
require . Equal ( t , "provider" , errorOwner )
require . Equal ( t , "upstream_http" , errorSource )
}
2026-05-26 17:19:09 +08:00
func TestParseOpsErrorResponsePreservesNestedStringCode ( t * testing . T ) {
parsed := parseOpsErrorResponse ([] byte ( `{"error":{"type":"permission_error","code":"GROUP_DELETED","message":"API Key 所属分组已删除"}}` ))
require . Equal ( t , "permission_error" , parsed . ErrorType )
require . Equal ( t , "GROUP_DELETED" , parsed . Code )
require . Equal ( t , "API Key 所属分组已删除" , parsed . Message )
}
2026-08-19 23:13:40 -07:00
func TestParseOpsErrorResponsePreservesStructuredTopLevelSemantics ( t * testing . T ) {
tests := [] struct {
name string
body string
wantType string
wantCode string
wantMsg string
}{
{
name : "model not found" ,
body : `{"type":"model_not_found","code":404,"message":"model unavailable"}` ,
wantType : "model_not_found" ,
wantCode : "404" ,
wantMsg : "model unavailable" ,
},
{
name : "string error" ,
body : `{"type":"service_unavailable","code":"temporarily_unavailable","error":"capacity exhausted"}` ,
wantType : "service_unavailable" ,
wantCode : "temporarily_unavailable" ,
wantMsg : "capacity exhausted" ,
},
}
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
parsed := parseOpsErrorResponse ([] byte ( tt . body ))
require . Equal ( t , tt . wantType , normalizeOpsErrorType ( parsed . ErrorType , parsed . Code ))
require . Equal ( t , tt . wantCode , parsed . Code )
require . Equal ( t , tt . wantMsg , parsed . Message )
})
}
}
func TestApplyOpsUpstreamFieldsUsesLastNonNilAttempt ( t * testing . T ) {
gin . SetMode ( gin . TestMode )
c , _ := gin . CreateTestContext ( httptest . NewRecorder ())
service . SetOpsUpstreamError ( c , http . StatusUnauthorized , "stale context" , "stale detail" )
c . Set ( service . OpsUpstreamErrorsKey , [] * service . OpsUpstreamErrorEvent {
{ UpstreamStatusCode : http . StatusTooManyRequests , Message : "first attempt" , Detail : "first detail" },
nil ,
{ UpstreamStatusCode : http . StatusServiceUnavailable , Message : "final attempt" , Detail : "final detail" },
nil ,
})
entry := & service . OpsInsertErrorLogInput {}
applyOpsUpstreamFieldsFromContext ( c , entry )
require . NotNil ( t , entry . UpstreamStatusCode )
require . Equal ( t , http . StatusServiceUnavailable , * entry . UpstreamStatusCode )
require . NotNil ( t , entry . UpstreamErrorMessage )
require . Equal ( t , "final attempt" , * entry . UpstreamErrorMessage )
require . NotNil ( t , entry . UpstreamErrorDetail )
require . Equal ( t , "final detail" , * entry . UpstreamErrorDetail )
require . Len ( t , entry . UpstreamErrors , 4 )
}
func TestApplyOpsUpstreamFieldsFinalStatuslessAttemptClearsStaleContext ( t * testing . T ) {
gin . SetMode ( gin . TestMode )
c , _ := gin . CreateTestContext ( httptest . NewRecorder ())
service . SetOpsUpstreamError ( c , http . StatusBadGateway , "stale response" , "stale body" )
c . Set ( service . OpsUpstreamErrorsKey , [] * service . OpsUpstreamErrorEvent {
{ UpstreamStatusCode : http . StatusBadGateway , Message : "first response" },
{ Kind : "request_error" , Message : "final transport failure" , Detail : "connection reset" },
})
entry := & service . OpsInsertErrorLogInput {}
applyOpsUpstreamFieldsFromContext ( c , entry )
require . Nil ( t , entry . UpstreamStatusCode )
require . NotNil ( t , entry . UpstreamErrorMessage )
require . Equal ( t , "final transport failure" , * entry . UpstreamErrorMessage )
require . NotNil ( t , entry . UpstreamErrorDetail )
require . Equal ( t , "connection reset" , * entry . UpstreamErrorDetail )
}
func TestOpsCaptureWriter_ProtocolLevelTerminalFrameDetection ( t * testing . T ) {
state := & opsCaptureWriterState { limit : opsCaptureWriterLimit }
chunks := [] string {
"event : response.failed\r\n" ,
"data: { \"response\" : { \"error\" : { \"message\" : \"busy\", \"code\" : \"service_unavailable\" } }," ,
" \"type\" : \"response.failed\" }\r\n\r\n" ,
}
for _ , chunk := range chunks {
state . captureResponseChunk ([] byte ( chunk ), http . StatusOK )
}
parsed := parseOpsErrorResponse ( state . buf . Bytes ())
require . True ( t , state . sseCapturing )
require . True ( t , parsed . StreamFailure )
require . Equal ( t , "service_unavailable_error" , parsed . ErrorType )
require . Equal ( t , "service_unavailable" , parsed . Code )
require . Equal ( t , "busy" , parsed . Message )
require . Equal ( t , http . StatusServiceUnavailable , inferStreamFailureStatus ( nil , parsed ))
}
func TestParseOpsSSEFailure_TopLevelErrorsAndUnknownStatus ( t * testing . T ) {
tests := [] struct {
name string
body string
wantType string
wantStatus int
}{
{
name : "top-level permission" ,
body : "event: error\ndata: {\"message\":\"denied\",\"code\":\"permission_denied\",\"type\":\"error\"}\n\n" ,
wantType : "permission_error" ,
wantStatus : http . StatusForbidden ,
},
{
name : "top-level unavailable" ,
body : "data: {\"message\":\"busy\",\"type\":\"error\",\"code\":\"service_unavailable\"}\n\n" ,
wantType : "service_unavailable_error" ,
wantStatus : http . StatusServiceUnavailable ,
},
{
name : "unknown terminal" ,
body : "event: response.failed\ndata: {\"type\":\"response.failed\",\"error\":{\"code\":\"new_provider_code\",\"message\":\"failed\"}}\n\n" ,
wantType : "upstream_error" ,
wantStatus : http . StatusBadGateway ,
},
{
name : "explicit terminal status" ,
body : "event: error\ndata: {\"type\":\"error\",\"status_code\":429,\"code\":\"new_rate_code\",\"message\":\"slow down\"}\n\n" ,
wantType : "api_error" ,
wantStatus : http . StatusTooManyRequests ,
},
}
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
parsed := parseOpsErrorResponse ([] byte ( tt . body ))
require . True ( t , parsed . StreamFailure )
require . Equal ( t , tt . wantType , parsed . ErrorType )
gin . SetMode ( gin . TestMode )
c , _ := gin . CreateTestContext ( httptest . NewRecorder ())
service . SetOpsUpstreamError ( c , http . StatusUnauthorized , "old attempt" , "" )
require . Equal ( t , tt . wantStatus , inferStreamFailureStatus ( c , parsed ), "terminal status must not inherit an earlier attempt" )
})
}
}
func TestOpsCaptureWriter_OversizedNonTerminalFrameRemainsBounded ( t * testing . T ) {
state := & opsCaptureWriterState { limit : opsCaptureWriterLimit }
state . captureResponseChunk ([] byte ( "data: " + strings . Repeat ( "x" , opsTerminalSSEFrameProbeLimit * 2 ) + "\n\n" ), http . StatusOK )
require . Empty ( t , state . buf . Bytes ())
require . LessOrEqual ( t , cap ( state . probe ), opsTerminalSSEFrameProbeLimit )
state . captureResponseChunk ([] byte ( "event: error\ndata: {\"type\":\"error\",\"code\":\"permission_denied\",\"message\":\"denied\"}\n\n" ), http . StatusOK )
require . True ( t , state . sseCapturing )
require . NotEmpty ( t , state . buf . Bytes ())
}
func TestOpsCaptureWriter_TerminalMetadataSurvivesBodyCaptureTruncation ( t * testing . T ) {
state := & opsCaptureWriterState { limit : opsCaptureWriterLimit }
frame := "event: response.failed\ndata: {\"padding\":\"" + strings . Repeat ( "x" , opsCaptureWriterLimit ) + "\",\"type\":\"response.failed\",\"error\":{\"code\":\"service_unavailable\",\"message\":\"busy\"}}\n\n"
state . captureResponseChunk ([] byte ( frame ), http . StatusOK )
state . finalizeResponseCapture ()
require . Len ( t , state . buf . Bytes (), opsCaptureWriterLimit )
require . True ( t , parseOpsErrorResponse ( state . buf . Bytes ()). StreamFailure , "the bounded parser must fail closed from the terminal event line" )
require . True ( t , state . terminalFound )
require . Equal ( t , "service_unavailable_error" , state . terminalError . ErrorType )
require . Equal ( t , "busy" , state . terminalError . Message )
}
func TestOpsErrorLoggerMiddleware_LargeTerminalFrameUsesEventFallback ( t * testing . T ) {
setupOpsErrorLogTestQueue ( t , 2 )
gin . SetMode ( gin . TestMode )
ops := service . NewOpsService ( nil , nil , nil , nil , nil , nil , nil , nil , nil , nil , nil )
router := gin . New ()
router . Use ( OpsErrorLoggerMiddleware ( ops ))
router . POST ( "/v1/responses" , func ( c * gin . Context ) {
c . Status ( http . StatusOK )
_ , _ = c . Writer . WriteString ( "event: response.failed\n" )
_ , _ = c . Writer . WriteString ( "data: {\"authorization\":\"Bearer must-not-persist\",\"padding\":\"" + strings . Repeat ( "x" , opsTerminalSSEFrameProbeLimit * 2 ) + "\"}" )
})
recorder := httptest . NewRecorder ()
router . ServeHTTP ( recorder , httptest . NewRequest ( http . MethodPost , "/v1/responses" , nil ))
require . Equal ( t , http . StatusOK , recorder . Code )
require . Equal ( t , int64 ( 1 ), OpsErrorLogQueueLength ())
job := <- opsErrorLogQueue
require . Equal ( t , http . StatusBadGateway , job . entry . StatusCode )
require . Equal ( t , "upstream_error" , job . entry . ErrorType )
require . Equal ( t , "upstream stream failed" , job . entry . ErrorMessage )
require . NotContains ( t , job . entry . ErrorMessage , "must-not-persist" )
require . NotContains ( t , job . entry . ErrorBody , "must-not-persist" )
require . Contains ( t , job . entry . ErrorBody , `"payload_truncated":true` )
}
func TestOpsErrorLoggerMiddleware_DetectsTerminalDataAtEOFWithoutBlankLine ( t * testing . T ) {
setupOpsErrorLogTestQueue ( t , 2 )
gin . SetMode ( gin . TestMode )
ops := service . NewOpsService ( nil , nil , nil , nil , nil , nil , nil , nil , nil , nil , nil )
router := gin . New ()
router . Use ( OpsErrorLoggerMiddleware ( ops ))
router . POST ( "/v1/responses" , func ( c * gin . Context ) {
c . Status ( http . StatusOK )
_ , _ = c . Writer . WriteString ( `data: {"message":"denied","code":"permission_denied","type":"error"}` )
})
recorder := httptest . NewRecorder ()
router . ServeHTTP ( recorder , httptest . NewRequest ( http . MethodPost , "/v1/responses" , nil ))
require . Equal ( t , http . StatusOK , recorder . Code )
require . Equal ( t , int64 ( 1 ), OpsErrorLogQueueLength ())
job := <- opsErrorLogQueue
require . Equal ( t , http . StatusForbidden , job . entry . StatusCode )
require . Equal ( t , "permission_error" , job . entry . ErrorType )
require . Equal ( t , "denied" , job . entry . ErrorMessage )
}
func TestOpsCaptureWriter_DetectsCROnlySSEFrame ( t * testing . T ) {
state := & opsCaptureWriterState { limit : opsCaptureWriterLimit }
state . captureResponseChunk ([] byte ( "data: {\"type\":\"error\",\"code\":\"service_unavailable\",\"message\":\"busy\"}\r\r" ), http . StatusOK )
require . True ( t , state . sseCapturing )
parsed := parseOpsErrorResponse ( state . buf . Bytes ())
require . True ( t , parsed . StreamFailure )
require . Equal ( t , "service_unavailable_error" , parsed . ErrorType )
}
func TestSanitizeOpsSSEDataForPersistence_RedactsJSONFields ( t * testing . T ) {
body := [] byte ( "event: error\ndata: {\"type\":\"error\",\"authorization\":\"Bearer secret\",\ndata: \"nested\":{\"api_key\":\"sk-secret\"}}\n\n" )
sanitized := sanitizeOpsSSEDataForPersistence ( body )
require . NotContains ( t , sanitized , "Bearer secret" )
require . NotContains ( t , sanitized , "sk-secret" )
require . Contains ( t , sanitized , `"authorization":"[REDACTED]"` )
require . Contains ( t , sanitized , `"api_key":"[REDACTED]"` )
}
func TestSanitizeOpsSSEDataForPersistence_DropsTruncatedJSONFragment ( t * testing . T ) {
body := [] byte ( "event: error\ndata: {\"type\":\"error\",\"authorization\":\"Bearer leaked" )
sanitized := sanitizeOpsSSEDataForPersistence ( body )
require . NotContains ( t , sanitized , "Bearer leaked" )
require . Contains ( t , sanitized , `data: {"payload_truncated":true}` )
}
func BenchmarkOpsCaptureWriterSuccessfulSSEFrames ( b * testing . B ) {
frame := [] byte ( "event: response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"delta\":\"ok\"}\n\n" )
state := & opsCaptureWriterState { limit : opsCaptureWriterLimit }
b . ReportAllocs ()
for i := 0 ; i < b . N ; i ++ {
state . captureResponseChunk ( frame , http . StatusOK )
}
if state . buf . Len () != 0 {
b . Fatal ( "successful frames must not be captured" )
}
}
2026-03-21 23:49:50 +08:00
func TestSetOpsEndpointContext_SetsContextKeys ( t * testing . T ) {
gin . SetMode ( gin . TestMode )
rec := httptest . NewRecorder ()
c , _ := gin . CreateTestContext ( rec )
c . Request = httptest . NewRequest ( http . MethodPost , "/v1/messages" , nil )
setOpsEndpointContext ( c , "claude-3-5-sonnet-20241022" , int16 ( 2 )) // stream
v , ok := c . Get ( opsUpstreamModelKey )
require . True ( t , ok )
2026-03-24 09:52:56 +08:00
vStr , ok := v .( string )
require . True ( t , ok )
require . Equal ( t , "claude-3-5-sonnet-20241022" , vStr )
2026-03-21 23:49:50 +08:00
rt , ok := c . Get ( opsRequestTypeKey )
require . True ( t , ok )
2026-03-24 09:52:56 +08:00
rtVal , ok := rt .( int16 )
require . True ( t , ok )
require . Equal ( t , int16 ( 2 ), rtVal )
2026-03-21 23:49:50 +08:00
}
func TestSetOpsEndpointContext_EmptyModelNotStored ( t * testing . T ) {
gin . SetMode ( gin . TestMode )
rec := httptest . NewRecorder ()
c , _ := gin . CreateTestContext ( rec )
c . Request = httptest . NewRequest ( http . MethodPost , "/v1/messages" , nil )
setOpsEndpointContext ( c , "" , int16 ( 1 ))
_ , ok := c . Get ( opsUpstreamModelKey )
require . False ( t , ok , "empty upstream model should not be stored" )
rt , ok := c . Get ( opsRequestTypeKey )
require . True ( t , ok )
2026-03-24 09:52:56 +08:00
rtVal , ok := rt .( int16 )
require . True ( t , ok )
require . Equal ( t , int16 ( 1 ), rtVal )
2026-03-21 23:49:50 +08:00
}
func TestSetOpsEndpointContext_NilContext ( t * testing . T ) {
require . NotPanics ( t , func () {
setOpsEndpointContext ( nil , "model" , int16 ( 1 ))
})
}
2026-06-01 16:10:35 +08:00
func TestGetOpsAPIKeyFallsBackToOpsFallbackKey ( t * testing . T ) {
gin . SetMode ( gin . TestMode )
rec := httptest . NewRecorder ()
c , _ := gin . CreateTestContext ( rec )
// 主 key 缺席(鉴权早退场景):返回 nil。
require . Nil ( t , getOpsAPIKey ( c ))
// 写入 ops 专用 fallback key 后应能取到,且带齐 user/group。
groupID := int64 ( 55 )
apiKey := & service . APIKey {
ID : 100 ,
GroupID : & groupID ,
User : & service . User { ID : 7 },
Group : & service . Group { ID : groupID , Platform : service . PlatformAnthropic },
}
c . Set ( string ( middleware2 . ContextKeyOpsFallbackAPIKey ), apiKey )
got := getOpsAPIKey ( c )
require . NotNil ( t , got )
require . Equal ( t , int64 ( 100 ), got . ID )
require . NotNil ( t , got . User )
require . Equal ( t , int64 ( 7 ), got . User . ID )
require . NotNil ( t , got . Group )
require . Equal ( t , service . PlatformAnthropic , got . Group . Platform )
}
func TestGetOpsAPIKeyPrefersPrimaryContextKey ( t * testing . T ) {
gin . SetMode ( gin . TestMode )
rec := httptest . NewRecorder ()
c , _ := gin . CreateTestContext ( rec )
primary := & service . APIKey { ID : 1 }
fallback := & service . APIKey { ID : 2 }
c . Set ( string ( middleware2 . ContextKeyAPIKey ), primary )
c . Set ( string ( middleware2 . ContextKeyOpsFallbackAPIKey ), fallback )
got := getOpsAPIKey ( c )
require . NotNil ( t , got )
require . Equal ( t , int64 ( 1 ), got . ID , "已鉴权请求应优先使用正式 api key" )
}
2026-09-05 15:58:26 +08:00
// 分组模型白名单入口拒绝:业务限流原因复用 local_model_configuration,但
// 携带 ingress 拒绝原因 model_not_allowed,阶段应保持自然分类(request /
// client),不落到 routing。
func TestClassifyOpsIngressModelNotAllowedKeepsRequestPhase ( t * testing . T ) {
gin . SetMode ( gin . TestMode )
c , _ := gin . CreateTestContext ( httptest . NewRecorder ())
service . MarkOpsClientBusinessLimited ( c , service . OpsClientBusinessLimitedReasonLocalModelConfiguration )
middleware2 . MarkIngressRejected ( c , middleware2 . IngressRejectModelNotAllowed )
phase , isBusinessLimited , errorOwner , errorSource := classifyOpsErrorLog (
c ,
"not_found_error" ,
`Model "gpt-4.1" is not available for this group` ,
"model_not_found" ,
http . StatusNotFound ,
)
require . Equal ( t , "request" , phase )
require . True ( t , isBusinessLimited )
require . Equal ( t , "client" , errorOwner )
require . Equal ( t , "client_request" , errorSource )
}
// 调度阶段的账号模型映射拒绝(无 ingress 标记)仍归类为 routing。
func TestClassifyOpsLocalModelConfigurationWithoutIngressMarkStaysRouting ( t * testing . T ) {
gin . SetMode ( gin . TestMode )
c , _ := gin . CreateTestContext ( httptest . NewRecorder ())
service . MarkOpsClientBusinessLimited ( c , service . OpsClientBusinessLimitedReasonLocalModelConfiguration )
phase , isBusinessLimited , errorOwner , _ := classifyOpsErrorLog (
c ,
"model_not_found" ,
"Model \"gpt-missing\" is not supported by any configured account in this group" ,
"" ,
http . StatusNotFound ,
)
require . Equal ( t , "routing" , phase )
require . True ( t , isBusinessLimited )
require . Equal ( t , "platform" , errorOwner )
}