mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-10-25 17:18:20 +08:00
- 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
75 lines
2.2 KiB
Go
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")
|
|
}
|
|
}
|