Files
CLIProxyAPI/internal/runtime/executor/codex_websockets_interrupt_test.go
Luis Pater 6331b38bba feat(openai): support response.interrupt in responses websocket handling
- Intercept and process `response.interrupt` control frames immediately on the downstream websocket reader
- Forward interrupts to active upstream Codex websocket sessions without starting a new turn or rewriting payload fields
- Support canceling in-flight HTTP turns via local interrupt handling
- Allow session-staged Home credentials to authorize interrupts

Closes: #6456
2026-10-08 21:32:26 +08:00

75 lines
2.2 KiB
Go

package executor
import (
"bytes"
"context"
"errors"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/gorilla/websocket"
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v8/sdk/cliproxy/executor"
)
func TestInterruptExecutionSessionRequiresActiveRead(t *testing.T) {
received := make(chan []byte, 1)
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
conn, errUpgrade := (&websocket.Upgrader{}).Upgrade(w, r, nil)
if errUpgrade != nil {
t.Errorf("upgrade: %v", errUpgrade)
return
}
defer func() { _ = conn.Close() }()
_ = conn.SetReadDeadline(time.Now().Add(2 * time.Second))
_, payload, errRead := conn.ReadMessage()
if errRead != nil {
return
}
received <- payload
}))
defer upstream.Close()
client, _, errDial := websocket.DefaultDialer.Dial("ws"+upstream.URL[len("http"):], nil)
if errDial != nil {
t.Fatal(errDial)
}
defer func() { _ = client.Close() }()
const sessionID = "interrupt-requires-active-read"
executor := NewCodexWebsocketsExecutor(nil)
defer executor.CloseExecutionSession(sessionID)
sess := executor.getOrCreateSession(sessionID)
sess.connMu.Lock()
sess.conn = client
sess.authID = "auth"
sess.wsURL = upstream.URL
sess.connMu.Unlock()
interrupt := []byte(`{"type":"response.interrupt","response_id":"r1","mode":"discard_partial_items"}`)
errIdle := executor.InterruptExecutionSession(context.Background(), sessionID, interrupt)
if !errors.Is(errIdle, cliproxyexecutor.ErrNoActiveUpstreamWebsocket) {
t.Fatalf("idle socket error = %v, want ErrNoActiveUpstreamWebsocket", errIdle)
}
select {
case payload := <-received:
t.Fatalf("idle socket was written: %s", payload)
case <-time.After(200 * time.Millisecond):
}
readCh := sess.activate(client)
defer sess.clearActive(client, readCh)
if errActive := executor.InterruptExecutionSession(context.Background(), sessionID, interrupt); errActive != nil {
t.Fatal(errActive)
}
select {
case payload := <-received:
if !bytes.Equal(payload, interrupt) {
t.Fatalf("active interrupt = %s", payload)
}
case <-time.After(2 * time.Second):
t.Fatal("active socket did not receive the interrupt")
}
}