diff --git a/internal/runtime/executor/codex_websockets_executor_test.go b/internal/runtime/executor/codex_websockets_executor_test.go index 47e957f3f..530a109ee 100644 --- a/internal/runtime/executor/codex_websockets_executor_test.go +++ b/internal/runtime/executor/codex_websockets_executor_test.go @@ -2667,15 +2667,11 @@ func TestCodexWebsockets_KeepalivePingDuringUpload_WithSession(t *testing.T) { return nil }) - // Start server reader loop so server processes control frames. - readErrCh := make(chan error, 1) + // The reader answers control frames until the client request arrives. + requestRead := make(chan struct{}) go func() { - for { - if _, _, errRead := conn.ReadMessage(); errRead != nil { - readErrCh <- errRead - return - } - } + _, _, _ = conn.ReadMessage() + close(requestRead) }() // Wait until client has entered writeMessage and is actively holding writeMu. @@ -2701,6 +2697,8 @@ func TestCodexWebsockets_KeepalivePingDuringUpload_WithSession(t *testing.T) { return } + // Reply only after the request arrives, so the reply and close cannot race the held write. + <-requestRead // Now send terminal response. respPayload := []byte(`{"type":"response.completed","response":{"id":"resp-1","status":"completed","output":[]}}`) _ = conn.WriteMessage(websocket.TextMessage, respPayload) @@ -2766,12 +2764,11 @@ func TestCodexWebsockets_KeepalivePingDuringUpload_Sessionless(t *testing.T) { return nil }) + // The reader answers control frames until the client request arrives. + requestRead := make(chan struct{}) go func() { - for { - if _, _, errRead := conn.ReadMessage(); errRead != nil { - return - } - } + _, _, _ = conn.ReadMessage() + close(requestRead) }() // Wait until client has entered writeMessage on sessionless path. @@ -2797,6 +2794,8 @@ func TestCodexWebsockets_KeepalivePingDuringUpload_Sessionless(t *testing.T) { return } + // Reply only after the request arrives, so the reply and close cannot race the held write. + <-requestRead respPayload := []byte(`{"type":"response.completed","response":{"id":"resp-1","status":"completed","output":[]}}`) _ = conn.WriteMessage(websocket.TextMessage, respPayload) })) @@ -2858,12 +2857,11 @@ func TestCodexWebsockets_KeepalivePingDuringUpload_NonstreamSessionless(t *testi return nil }) + // The reader answers control frames until the client request arrives. + requestRead := make(chan struct{}) go func() { - for { - if _, _, errRead := conn.ReadMessage(); errRead != nil { - return - } - } + _, _, _ = conn.ReadMessage() + close(requestRead) }() // Wait until client has entered writeMessage on nonstream path. @@ -2889,6 +2887,8 @@ func TestCodexWebsockets_KeepalivePingDuringUpload_NonstreamSessionless(t *testi return } + // Reply only after the request arrives, so the reply and close cannot race the held write. + <-requestRead respPayload := []byte(`{"type":"response.completed","response":{"id":"resp-1","status":"completed","output":[]}}`) _ = conn.WriteMessage(websocket.TextMessage, respPayload) })) diff --git a/internal/runtime/executor/websocket_session_target_test.go b/internal/runtime/executor/websocket_session_target_test.go index 2422bb0e8..382faac2f 100644 --- a/internal/runtime/executor/websocket_session_target_test.go +++ b/internal/runtime/executor/websocket_session_target_test.go @@ -232,12 +232,13 @@ func TestWebsocketRetryBindFailureClearsActiveSessionState(t *testing.T) { upgrader := websocket.Upgrader{CheckOrigin: func(*http.Request) bool { return true }} var connections atomic.Int32 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Count before the handshake completes so numbers follow dial order. + connection := connections.Add(1) conn, errUpgrade := upgrader.Upgrade(w, r, nil) if errUpgrade != nil { t.Errorf("upgrade websocket: %v", errUpgrade) return } - connection := connections.Add(1) defer func() { _ = conn.Close() }() if connection == 1 { _, _, _ = conn.ReadMessage() diff --git a/internal/runtime/executor/xai_websockets_executor_test.go b/internal/runtime/executor/xai_websockets_executor_test.go index ebf42156f..de16dd133 100644 --- a/internal/runtime/executor/xai_websockets_executor_test.go +++ b/internal/runtime/executor/xai_websockets_executor_test.go @@ -2214,12 +2214,11 @@ func TestXAIWebsockets_KeepalivePingDuringUpload_WithSession(t *testing.T) { return nil }) + // The reader answers control frames until the client request arrives. + requestRead := make(chan struct{}) go func() { - for { - if _, _, errReadLoop := conn.ReadMessage(); errReadLoop != nil { - return - } - } + _, _, _ = conn.ReadMessage() + close(requestRead) }() select { @@ -2242,6 +2241,8 @@ func TestXAIWebsockets_KeepalivePingDuringUpload_WithSession(t *testing.T) { return } + // Reply only after the request arrives, so the reply and close cannot race the held write. + <-requestRead respPayload := []byte(`{"type":"response.done","response":{"id":"resp-1","status":"completed","output":[]}}`) _ = conn.WriteMessage(websocket.TextMessage, respPayload) })) @@ -2310,12 +2311,11 @@ func TestXAIWebsockets_KeepalivePingDuringUpload_Sessionless(t *testing.T) { return nil }) + // The reader answers control frames until the client request arrives. + requestRead := make(chan struct{}) go func() { - for { - if _, _, errReadLoop := conn.ReadMessage(); errReadLoop != nil { - return - } - } + _, _, _ = conn.ReadMessage() + close(requestRead) }() select { @@ -2338,6 +2338,8 @@ func TestXAIWebsockets_KeepalivePingDuringUpload_Sessionless(t *testing.T) { return } + // Reply only after the request arrives, so the reply and close cannot race the held write. + <-requestRead respPayload := []byte(`{"type":"response.done","response":{"id":"resp-1","status":"completed","output":[]}}`) _ = conn.WriteMessage(websocket.TextMessage, respPayload) }))