@@ -3,6 +3,7 @@ package executor
33import (
44 "bytes"
55 "context"
6+ "errors"
67 "net/http"
78 "net/http/httptest"
89 "testing"
@@ -91,6 +92,97 @@ func TestCodexWebsocketsExecutePreservesPreviousResponseIDUpstream(t *testing.T)
9192 }
9293}
9394
95+ func TestCodexWebsocketSessionRedialsWhenAuthChanges (t * testing.T ) {
96+ upgrader := websocket.Upgrader {CheckOrigin : func (* http.Request ) bool { return true }}
97+ authHeaders := make (chan string , 2 )
98+ server := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
99+ conn , err := upgrader .Upgrade (w , r , nil )
100+ if err != nil {
101+ t .Errorf ("upgrade websocket: %v" , err )
102+ return
103+ }
104+ defer func () { _ = conn .Close () }()
105+
106+ for {
107+ msgType , _ , errRead := conn .ReadMessage ()
108+ if errRead != nil {
109+ return
110+ }
111+ if msgType != websocket .TextMessage {
112+ continue
113+ }
114+ authHeaders <- r .Header .Get ("Authorization" )
115+ completed := []byte (`{"type":"response.completed","response":{"id":"resp-1","output":[],"usage":{"input_tokens":0,"output_tokens":0,"total_tokens":0}}}` )
116+ if errWrite := conn .WriteMessage (websocket .TextMessage , completed ); errWrite != nil {
117+ return
118+ }
119+ }
120+ }))
121+ defer server .Close ()
122+
123+ exec := NewCodexWebsocketsExecutor (& config.Config {})
124+ sessionID := "session-redial-auth"
125+ defer exec .CloseExecutionSession (sessionID )
126+
127+ req := cliproxyexecutor.Request {
128+ Model : "gpt-5-codex" ,
129+ Payload : []byte (`{"model":"gpt-5-codex","input":[]}` ),
130+ }
131+ opts := cliproxyexecutor.Options {
132+ SourceFormat : sdktranslator .FromString ("openai-response" ),
133+ Metadata : map [string ]any {
134+ cliproxyexecutor .ExecutionSessionMetadataKey : sessionID ,
135+ },
136+ }
137+ auths := []struct {
138+ auth * cliproxyauth.Auth
139+ want string
140+ }{
141+ {
142+ auth : & cliproxyauth.Auth {ID : "auth-a" , Attributes : map [string ]string {"api_key" : "sk-a" , "base_url" : server .URL }},
143+ want : "Bearer sk-a" ,
144+ },
145+ {
146+ auth : & cliproxyauth.Auth {ID : "auth-b" , Attributes : map [string ]string {"api_key" : "sk-b" , "base_url" : server .URL }},
147+ want : "Bearer sk-b" ,
148+ },
149+ }
150+
151+ for i := range auths {
152+ streamResult , err := exec .ExecuteStream (context .Background (), auths [i ].auth , req , opts )
153+ if err != nil {
154+ t .Fatalf ("ExecuteStream(%s) error = %v" , auths [i ].auth .ID , err )
155+ }
156+ for chunk := range streamResult .Chunks {
157+ if chunk .Err != nil {
158+ t .Fatalf ("stream chunk error for %s: %v" , auths [i ].auth .ID , chunk .Err )
159+ }
160+ }
161+
162+ select {
163+ case got := <- authHeaders :
164+ if got != auths [i ].want {
165+ t .Fatalf ("request %d Authorization = %q, want %q" , i + 1 , got , auths [i ].want )
166+ }
167+ case <- time .After (5 * time .Second ):
168+ t .Fatalf ("timed out waiting for upstream auth header %d" , i + 1 )
169+ }
170+ }
171+ }
172+
173+ func TestShouldNotifyCodexUpstreamDisconnectSkipsRecoverableStatus (t * testing.T ) {
174+ err , ok := parseCodexWebsocketError ([]byte (`{"type":"error","status":429,"error":{"message":"quota exhausted"}}` ))
175+ if ! ok {
176+ t .Fatalf ("expected websocket status error" )
177+ }
178+ if shouldNotifyCodexUpstreamDisconnect (err ) {
179+ t .Fatalf ("recoverable websocket status should not force downstream disconnect" )
180+ }
181+ if ! shouldNotifyCodexUpstreamDisconnect (errors .New ("network reset" )) {
182+ t .Fatalf ("network disconnect should still close downstream" )
183+ }
184+ }
185+
94186func TestApplyCodexWebsocketHeadersDefaultsToCurrentResponsesBeta (t * testing.T ) {
95187 headers := applyCodexWebsocketHeaders (context .Background (), http.Header {}, nil , "" , nil )
96188
0 commit comments