124 lines
3.5 KiB
Go
124 lines
3.5 KiB
Go
package testsupport
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"encoding/json"
|
||
|
|
"fmt"
|
||
|
|
"net/http"
|
||
|
|
"time"
|
||
|
|
)
|
||
|
|
|
||
|
|
// Stub response paths. testdata/config/harness.yaml points every upstream at the
|
||
|
|
// one stub server, so the response shape is selected by path, not by port.
|
||
|
|
const (
|
||
|
|
PathJSON = "/stub/json"
|
||
|
|
PathSSE = "/stub/sse"
|
||
|
|
PathChunked = "/stub/chunked"
|
||
|
|
PathSlow = "/stub/slow"
|
||
|
|
)
|
||
|
|
|
||
|
|
// SSEGap is the pause between SSE events. It exists so a test can observe that
|
||
|
|
// chunks arrive incrementally rather than all at once at the end.
|
||
|
|
const SSEGap = 20 * time.Millisecond
|
||
|
|
|
||
|
|
// SlowDelay is how long PathSlow waits before writing anything.
|
||
|
|
const SlowDelay = 250 * time.Millisecond
|
||
|
|
|
||
|
|
// SSETokens are the tokens PathSSE emits, one event per token, followed by the
|
||
|
|
// [DONE] sentinel that OpenAI-shaped clients expect.
|
||
|
|
var SSETokens = []string{"Hello", " ", "world", "!"}
|
||
|
|
|
||
|
|
// ChunkedBodies are the pieces PathChunked writes, each flushed separately so
|
||
|
|
// the response goes out with Transfer-Encoding: chunked.
|
||
|
|
var ChunkedBodies = []string{"first\n", "second\n", "third\n"}
|
||
|
|
|
||
|
|
// newStubHandler builds the mux served by the harness. Every handler writes a
|
||
|
|
// deterministic body so tests can assert on exact bytes.
|
||
|
|
func newStubHandler() http.Handler {
|
||
|
|
mux := http.NewServeMux()
|
||
|
|
mux.HandleFunc(PathJSON, stubJSON)
|
||
|
|
mux.HandleFunc(PathSSE, stubSSE)
|
||
|
|
mux.HandleFunc(PathChunked, stubChunked)
|
||
|
|
mux.HandleFunc(PathSlow, stubSlow)
|
||
|
|
return mux
|
||
|
|
}
|
||
|
|
|
||
|
|
// stubJSON serves a fixed JSON body.
|
||
|
|
func stubJSON(w http.ResponseWriter, _ *http.Request) {
|
||
|
|
w.Header().Set("Content-Type", "application/json")
|
||
|
|
w.WriteHeader(http.StatusOK)
|
||
|
|
// Encode failure here means the client hung up mid-write; the connection is
|
||
|
|
// already gone, so there is nothing to report and no header left to change.
|
||
|
|
_ = json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
|
||
|
|
}
|
||
|
|
|
||
|
|
// stubSSE streams one event per token, flushing after each. It returns early
|
||
|
|
// when the client disconnects, so a mid-response disconnect test can assert on
|
||
|
|
// how many tokens the upstream actually managed to emit.
|
||
|
|
func stubSSE(w http.ResponseWriter, r *http.Request) {
|
||
|
|
w.Header().Set("Content-Type", "text/event-stream")
|
||
|
|
w.Header().Set("Cache-Control", "no-cache")
|
||
|
|
w.Header().Set("Connection", "keep-alive")
|
||
|
|
w.WriteHeader(http.StatusOK)
|
||
|
|
|
||
|
|
rc := http.NewResponseController(w)
|
||
|
|
if err := rc.Flush(); err != nil {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
for _, tok := range SSETokens {
|
||
|
|
select {
|
||
|
|
case <-r.Context().Done():
|
||
|
|
return
|
||
|
|
case <-time.After(SSEGap):
|
||
|
|
}
|
||
|
|
|
||
|
|
if _, err := fmt.Fprintf(w, "data: %s\n\n", tok); err != nil {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
if err := rc.Flush(); err != nil {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if _, err := fmt.Fprint(w, "data: [DONE]\n\n"); err != nil {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
_ = rc.Flush()
|
||
|
|
}
|
||
|
|
|
||
|
|
// stubChunked writes several pieces with a flush between each, producing a
|
||
|
|
// chunked transfer with no Content-Length.
|
||
|
|
func stubChunked(w http.ResponseWriter, r *http.Request) {
|
||
|
|
w.Header().Set("Content-Type", "text/plain")
|
||
|
|
w.WriteHeader(http.StatusOK)
|
||
|
|
|
||
|
|
rc := http.NewResponseController(w)
|
||
|
|
for _, body := range ChunkedBodies {
|
||
|
|
select {
|
||
|
|
case <-r.Context().Done():
|
||
|
|
return
|
||
|
|
case <-time.After(SSEGap):
|
||
|
|
}
|
||
|
|
|
||
|
|
if _, err := fmt.Fprint(w, body); err != nil {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
if err := rc.Flush(); err != nil {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// stubSlow waits SlowDelay before responding at all, for timeout tests.
|
||
|
|
func stubSlow(w http.ResponseWriter, r *http.Request) {
|
||
|
|
select {
|
||
|
|
case <-r.Context().Done():
|
||
|
|
return
|
||
|
|
case <-time.After(SlowDelay):
|
||
|
|
}
|
||
|
|
|
||
|
|
w.Header().Set("Content-Type", "text/plain")
|
||
|
|
w.WriteHeader(http.StatusOK)
|
||
|
|
_, _ = fmt.Fprint(w, "slow\n")
|
||
|
|
}
|