Files

1019 lines
31 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package helper
import (
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"strconv"
"strings"
"sync"
"testing"
"github.com/qianfree/team-api/relay/dto"
)
// TestSetEventStreamHeaders_Idempotency 测试 SSE 头幂等性保护
func TestSetEventStreamHeaders_Idempotency(t *testing.T) {
tests := []struct {
name string
useSafeWriter bool
callCount int
validateHeader func(t *testing.T, w *httptest.ResponseRecorder)
}{
{
name: "SafeWriter 多次调用幂等性保护",
useSafeWriter: true,
callCount: 3,
validateHeader: func(t *testing.T, w *httptest.ResponseRecorder) {
if ct := w.Header().Get("Content-Type"); ct != "text/event-stream" {
t.Errorf("Content-Type = %v, want text/event-stream", ct)
}
if te := w.Header().Get("Transfer-Encoding"); te != "chunked" {
t.Errorf("Transfer-Encoding = %v, want chunked", te)
}
if xab := w.Header().Get("X-Accel-Buffering"); xab != "no" {
t.Errorf("X-Accel-Buffering = %v, want no", xab)
}
},
},
{
name: "原生 ResponseWriter 单次调用正常工作",
useSafeWriter: false,
callCount: 1,
validateHeader: func(t *testing.T, w *httptest.ResponseRecorder) {
if ct := w.Header().Get("Content-Type"); ct != "text/event-stream" {
t.Errorf("Content-Type = %v, want text/event-stream", ct)
}
if te := w.Header().Get("Transfer-Encoding"); te != "chunked" {
t.Errorf("Transfer-Encoding = %v, want chunked", te)
}
},
},
{
name: "原生 ResponseWriter 多次调用(httptest.ResponseRecorder 自带保护)",
useSafeWriter: false,
callCount: 2,
validateHeader: func(t *testing.T, w *httptest.ResponseRecorder) {
// httptest.ResponseRecorder 本身有幂等性保护,验证头仍然正确
if ct := w.Header().Get("Content-Type"); ct != "text/event-stream" {
t.Errorf("Content-Type = %v, want text/event-stream", ct)
}
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
w := httptest.NewRecorder()
var writer http.ResponseWriter = w
if tt.useSafeWriter {
writer = NewSafeWriter(w)
}
// 多次调用 SetEventStreamHeaders
for i := 0; i < tt.callCount; i++ {
SetEventStreamHeaders(writer)
}
tt.validateHeader(t, w)
})
}
}
// TestSetEventStreamHeaders_AllHeaders 测试所有必要的 SSE 头都被正确设置
func TestSetEventStreamHeaders_AllHeaders(t *testing.T) {
w := httptest.NewRecorder()
sw := NewSafeWriter(w)
SetEventStreamHeaders(sw)
expectedHeaders := map[string]string{
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"Transfer-Encoding": "chunked",
"X-Accel-Buffering": "no",
}
for key, expectedValue := range expectedHeaders {
actualValue := w.Header().Get(key)
if actualValue != expectedValue {
t.Errorf("Header %s = %v, want %v", key, actualValue, expectedValue)
}
}
// 验证状态码
if w.Code != http.StatusOK {
t.Errorf("StatusCode = %v, want %v", w.Code, http.StatusOK)
}
}
// TestSafeWriter_ConcurrentSafety 测试 SafeWriter 的并发安全性
func TestSafeWriter_ConcurrentSafety(t *testing.T) {
w := httptest.NewRecorder()
sw := NewSafeWriter(w)
SetEventStreamHeaders(sw)
// 并发写入 SSE 数据
done := make(chan bool)
for i := 0; i < 10; i++ {
go func(n int) {
defer func() { done <- true }()
for j := 0; j < 100; j++ {
_ = WriteSSEData(sw, "test data")
}
}(i)
}
// 等待所有 goroutine 完成
for i := 0; i < 10; i++ {
<-done
}
// 验证没有 panic 且数据写入成功
body := w.Body.String()
if !strings.Contains(body, "data: test data") {
t.Error("Expected SSE data not found in response body")
}
}
// TestWriteSSEData 测试 SSE 数据写入
func TestWriteSSEData(t *testing.T) {
w := httptest.NewRecorder()
sw := NewSafeWriter(w)
SetEventStreamHeaders(sw)
testData := "Hello, SSE!"
err := WriteSSEData(sw, testData)
if err != nil {
t.Fatalf("WriteSSEData failed: %v", err)
}
body := w.Body.String()
expected := "data: " + testData + "\n\n"
if body != expected {
t.Errorf("Body = %q, want %q", body, expected)
}
}
// TestWriteSSEEvent 测试带事件名的 SSE 写入
func TestWriteSSEEvent(t *testing.T) {
w := httptest.NewRecorder()
sw := NewSafeWriter(w)
SetEventStreamHeaders(sw)
err := WriteSSEEvent(sw, "message", `{"content":"test"}`)
if err != nil {
t.Fatalf("WriteSSEEvent failed: %v", err)
}
body := w.Body.String()
expected := "event: message\ndata: {\"content\":\"test\"}\n\n"
if body != expected {
t.Errorf("Body = %q, want %q", body, expected)
}
}
// TestWriteSSEPing 测试 SSE 保活注释
func TestWriteSSEPing(t *testing.T) {
w := httptest.NewRecorder()
sw := NewSafeWriter(w)
SetEventStreamHeaders(sw)
err := WriteSSEPing(sw)
if err != nil {
t.Fatalf("WriteSSEPing failed: %v", err)
}
body := w.Body.String()
expected := ": PING\n\n"
if body != expected {
t.Errorf("Body = %q, want %q", body, expected)
}
}
// sseEvent 一个被客户端派发出去的 SSE 事件
type sseEvent struct {
event string
data string
}
// decodeSSE 按 SSE 规范模拟客户端解码:`:` 开头为注释忽略,空行即派发当前事件。
// 用于断言网关吐出的字节在客户端眼里是什么,而不是只比对原始字符串。
func decodeSSE(raw string) []sseEvent {
var events []sseEvent
cur := sseEvent{}
var dataLines []string
dispatch := func() {
if cur.event == "" && len(dataLines) == 0 {
return
}
cur.data = strings.Join(dataLines, "\n")
events = append(events, cur)
cur = sseEvent{}
dataLines = nil
}
for _, line := range strings.Split(raw, "\n") {
line = strings.TrimSuffix(line, "\r")
switch {
case line == "":
dispatch()
case strings.HasPrefix(line, ":"):
// 注释(保活 ping),忽略
case strings.HasPrefix(line, "event:"):
cur.event = strings.TrimSpace(strings.TrimPrefix(line, "event:"))
case strings.HasPrefix(line, "data:"):
dataLines = append(dataLines, strings.TrimSpace(strings.TrimPrefix(line, "data:")))
}
}
dispatch()
return events
}
// assertNoEmptyDataEvent 断言没有「有事件名但 data 为空」的事件 —— 这正是客户端
// 对空串做 JSON.parse 报 "JSON Parse error: Unexpected EOF" 并中止请求的成因。
func assertNoEmptyDataEvent(t *testing.T, raw string) {
t.Helper()
for i, ev := range decodeSSE(raw) {
if ev.event != "" && ev.data == "" {
t.Errorf("事件 #%d (event=%q) 的 data 为空,客户端 JSON.parse 会失败;原始输出:\n%s",
i, ev.event, raw)
}
}
}
// TestSafeWriter_LineByLineWriteIsSplitByPing 对照用例:逐行写 SSE 时,
// SafeWriter 挡不住保活 ping 把自带空行插进帧中间 —— 半帧被提前派发,
// 客户端拿到 data 为空的事件。这是 SSEFrameWriter 存在的理由,也证明
// assertNoEmptyDataEvent 确实能抓到该缺陷。
func TestSafeWriter_LineByLineWriteIsSplitByPing(t *testing.T) {
rec := httptest.NewRecorder()
sw := NewSafeWriter(rec)
_, _ = sw.Write([]byte("event: content_block_delta\n"))
// 主循环阻塞在读上游下一行时,ping goroutine 抢到了写入
_ = WriteSSEPing(sw)
_, _ = sw.Write([]byte(`data: {"type":"content_block_delta"}` + "\n"))
_, _ = sw.Write([]byte("\n"))
events := decodeSSE(rec.Body.String())
if len(events) == 0 || events[0].data != "" {
t.Fatalf("对照用例失效:期望逐行写会派发出空 data 事件,实际 events=%+v", events)
}
}
// TestSSEFrameWriter_PingCannotSplitFrame 回归:改用 SSEFrameWriter 后,
// 帧攒齐才出网,ping 只能落在帧边界,客户端不会再收到空 data 事件。
func TestSSEFrameWriter_PingCannotSplitFrame(t *testing.T) {
rec := httptest.NewRecorder()
sw := NewSafeWriter(rec)
frame := NewSSEFrameWriter(sw)
if err := frame.WriteLine("event: content_block_delta\n"); err != nil {
t.Fatalf("WriteLine failed: %v", err)
}
// 半帧必须还留在缓冲里,一个字节都不能出网
if rec.Body.Len() != 0 {
t.Errorf("半帧提前出网: %q", rec.Body.String())
}
if frame.Buffered() == 0 {
t.Error("Buffered() = 0, 期望半帧仍在缓冲中")
}
// ping goroutine 此刻抢到写入
if err := WriteSSEPing(sw); err != nil {
t.Fatalf("WriteSSEPing failed: %v", err)
}
if err := frame.WriteLine(`data: {"type":"content_block_delta"}` + "\n"); err != nil {
t.Fatalf("WriteLine failed: %v", err)
}
if err := frame.WriteLine("\n"); err != nil {
t.Fatalf("WriteLine failed: %v", err)
}
body := rec.Body.String()
assertNoEmptyDataEvent(t, body)
events := decodeSSE(body)
if len(events) != 1 {
t.Fatalf("派发事件数 = %d, want 1; body=%q", len(events), body)
}
if events[0].event != "content_block_delta" || events[0].data != `{"type":"content_block_delta"}` {
t.Errorf("事件 = %+v, 与写入内容不符", events[0])
}
// ping 必须整体落在帧之前,不能夹在帧内部
if !strings.HasPrefix(body, ": PING\n\n") {
t.Errorf("ping 未落在帧边界: %q", body)
}
}
// TestSSEFrameWriter_ConcurrentPingDuringFrames 并发压力下的帧完整性:
// 主循环持续逐行写帧,ping goroutine 同时高频写入,客户端解码后不得出现空 data 事件。
func TestSSEFrameWriter_ConcurrentPingDuringFrames(t *testing.T) {
rec := httptest.NewRecorder()
sw := NewSafeWriter(rec)
frame := NewSSEFrameWriter(sw)
stopPing := make(chan struct{})
pingDone := make(chan struct{})
go func() {
defer close(pingDone)
for {
select {
case <-stopPing:
return
default:
_ = WriteSSEPing(sw)
}
}
}()
for i := 0; i < 500; i++ {
if err := frame.WriteLine("event: content_block_delta\n"); err != nil {
t.Fatalf("WriteLine failed: %v", err)
}
if err := frame.WriteLine(`data: {"i":` + strconv.Itoa(i) + "}\n"); err != nil {
t.Fatalf("WriteLine failed: %v", err)
}
if err := frame.WriteLine("\n"); err != nil {
t.Fatalf("WriteLine failed: %v", err)
}
}
close(stopPing)
<-pingDone
body := rec.Body.String()
assertNoEmptyDataEvent(t, body)
events := decodeSSE(body)
if len(events) != 500 {
t.Fatalf("派发事件数 = %d, want 500", len(events))
}
for i, ev := range events {
if want := `{"i":` + strconv.Itoa(i) + "}"; ev.data != want {
t.Fatalf("事件 #%d data = %q, want %q(帧内容错位)", i, ev.data, want)
}
}
}
// TestSSEFrameWriter_CRLFBoundary 上游用 CRLF 行尾时同样按帧冲刷。
// 旧实现只认 len(line)==1 && line[0]=='\n',CRLF 上游会导致整条流从不 Flush。
func TestSSEFrameWriter_CRLFBoundary(t *testing.T) {
rec := httptest.NewRecorder()
frame := NewSSEFrameWriter(NewSafeWriter(rec))
_ = frame.WriteLine("event: ping\r\n")
if rec.Body.Len() != 0 {
t.Errorf("半帧提前出网: %q", rec.Body.String())
}
_ = frame.WriteLine("data: {}\r\n")
_ = frame.WriteLine("\r\n")
if frame.Buffered() != 0 {
t.Errorf("CRLF 空行未被识别为帧边界,仍有 %d 字节滞留", frame.Buffered())
}
if got := rec.Body.String(); got != "event: ping\r\ndata: {}\r\n\r\n" {
t.Errorf("Body = %q", got)
}
}
// TestSSEFrameWriter_FlushPartial 上游未以空行结尾时,残留必须冲刷,不能丢帧。
func TestSSEFrameWriter_FlushPartial(t *testing.T) {
rec := httptest.NewRecorder()
frame := NewSSEFrameWriter(NewSafeWriter(rec))
_ = frame.WriteLine("data: {\"last\":true}\n")
if rec.Body.Len() != 0 {
t.Fatalf("无空行结尾的帧不应提前出网: %q", rec.Body.String())
}
if err := frame.FlushPartial(); err != nil {
t.Fatalf("FlushPartial failed: %v", err)
}
if got := rec.Body.String(); got != "data: {\"last\":true}\n" {
t.Errorf("Body = %q, 最后一帧丢失", got)
}
// 空缓冲重复调用应为 no-op
if err := frame.FlushPartial(); err != nil {
t.Errorf("空缓冲 FlushPartial 应为 no-op, got %v", err)
}
}
// TestSSEFrameWriter_Discard 中断时丢弃半帧,不把不完整的帧写给客户端。
func TestSSEFrameWriter_Discard(t *testing.T) {
rec := httptest.NewRecorder()
frame := NewSSEFrameWriter(NewSafeWriter(rec))
_ = frame.WriteLine("event: content_block_delta\n")
frame.Discard()
if frame.Buffered() != 0 {
t.Errorf("Discard 后 Buffered() = %d, want 0", frame.Buffered())
}
if err := frame.FlushPartial(); err != nil {
t.Fatalf("FlushPartial failed: %v", err)
}
if rec.Body.Len() != 0 {
t.Errorf("被丢弃的半帧仍然出网: %q", rec.Body.String())
}
}
// TestSSEFrameWriter_MaxBufDegrades 上游异常(始终不发空行)时按上限降级冲刷,
// 不把整条流攒进内存。
func TestSSEFrameWriter_MaxBufDegrades(t *testing.T) {
rec := httptest.NewRecorder()
frame := NewSSEFrameWriter(NewSafeWriter(rec))
line := "data: " + strings.Repeat("x", 64*1024) + "\n"
for i := 0; i < sseFrameMaxBuf/len(line)+2; i++ {
if err := frame.WriteLine(line); err != nil {
t.Fatalf("WriteLine failed: %v", err)
}
}
if frame.Buffered() >= sseFrameMaxBuf {
t.Errorf("Buffered() = %d, 超过上限仍未降级冲刷", frame.Buffered())
}
if rec.Body.Len() == 0 {
t.Error("超过上限后仍未向客户端写出任何数据")
}
}
// ===== 池化帧写入器 =====
// benchClaudeDelta 构造一个典型的 Claude 流式增量事件,作为基准与парity 测试的载荷
func benchClaudeDelta() *dto.ClaudeResponse {
text := "这是一段典型长度的流式增量文本内容"
idx := 0
return &dto.ClaudeResponse{
Type: "content_block_delta",
Index: &idx,
Delta: &dto.ClaudeDelta{Type: "text_delta", Text: &text},
}
}
// TestWriteSSEEventJSON_ByteParityWithMarshalFprintf 池化路径的输出必须与
// 「json.Marshal → string → WriteSSEEvent」逐字节一致(含 HTML 转义行为),
// 否则等于悄悄改了对外协议。
func TestWriteSSEEventJSON_ByteParityWithMarshalFprintf(t *testing.T) {
payloads := []any{
benchClaudeDelta(),
&dto.ClaudeResponse{Type: "message_stop"},
// HTML 字符:json.Marshal 与 json.Encoder 默认都转义,此处锁定该行为
map[string]any{"text": `a<b>c&d`, "n": 1.5, "null": nil},
map[string]any{"empty": ""},
}
for i, p := range payloads {
old := httptest.NewRecorder()
data, err := json.Marshal(p)
if err != nil {
t.Fatalf("payload #%d marshal failed: %v", i, err)
}
if err := WriteSSEEvent(old, "content_block_delta", string(data)); err != nil {
t.Fatalf("WriteSSEEvent failed: %v", err)
}
pooled := httptest.NewRecorder()
if err := WriteSSEEventJSON(pooled, "content_block_delta", p); err != nil {
t.Fatalf("WriteSSEEventJSON failed: %v", err)
}
if old.Body.String() != pooled.Body.String() {
t.Errorf("payload #%d 输出不一致:\nold: %q\npooled: %q", i, old.Body.String(), pooled.Body.String())
}
}
}
// TestWriteSSEDataJSON_ByteParity data-only 帧同样要求逐字节一致。
func TestWriteSSEDataJSON_ByteParity(t *testing.T) {
p := benchClaudeDelta()
old := httptest.NewRecorder()
data, _ := json.Marshal(p)
if err := WriteSSEData(old, string(data)); err != nil {
t.Fatalf("WriteSSEData failed: %v", err)
}
pooled := httptest.NewRecorder()
if err := WriteSSEDataJSON(pooled, p); err != nil {
t.Fatalf("WriteSSEDataJSON failed: %v", err)
}
if old.Body.String() != pooled.Body.String() {
t.Errorf("输出不一致:\nold: %q\npooled: %q", old.Body.String(), pooled.Body.String())
}
}
// TestWriteSSEEventJSON_MarshalFailureWritesNothing 序列化失败时必须一个字节都不出网,
// 且错误可被 errors.Is 识别 —— 半截帧或空 data 帧会让客户端整条请求失败。
func TestWriteSSEEventJSON_MarshalFailureWritesNothing(t *testing.T) {
rec := httptest.NewRecorder()
// chan 无法被 encoding/json 序列化
err := WriteSSEEventJSON(rec, "content_block_delta", map[string]any{"bad": make(chan int)})
if !errors.Is(err, ErrSSEPayloadMarshal) {
t.Fatalf("err = %v, want ErrSSEPayloadMarshal", err)
}
if rec.Body.Len() != 0 {
t.Errorf("序列化失败仍写出了 %q", rec.Body.String())
}
// 失败后缓冲归池,不得污染后续事件
rec2 := httptest.NewRecorder()
if err := WriteSSEEventJSON(rec2, "content_block_delta", benchClaudeDelta()); err != nil {
t.Fatalf("后续写入失败: %v", err)
}
if !strings.HasPrefix(rec2.Body.String(), "event: content_block_delta\ndata: {") {
t.Errorf("池被上一次失败污染: %q", rec2.Body.String())
}
}
// TestWriteSSEEventJSON_FrameIsSingleWrite 整帧必须单次 Write 写出 ——
// 这是保活 ping 无法插进帧中间的前提。
func TestWriteSSEEventJSON_FrameIsSingleWrite(t *testing.T) {
w := &countingWriter{}
if err := WriteSSEEventJSON(w, "content_block_delta", benchClaudeDelta()); err != nil {
t.Fatalf("WriteSSEEventJSON failed: %v", err)
}
if w.writes != 1 {
t.Errorf("Write 调用次数 = %d, want 1(多次写出会给 ping 留出插入窗口)", w.writes)
}
}
// countingWriter 统计 Write 调用次数
type countingWriter struct {
header http.Header
writes int
}
func (c *countingWriter) Header() http.Header {
if c.header == nil {
c.header = http.Header{}
}
return c.header
}
func (c *countingWriter) WriteHeader(int) {}
func (c *countingWriter) Write(p []byte) (int, error) {
c.writes++
return len(p), nil
}
// discardWriter 丢弃写入内容的 ResponseWriter,让基准只测序列化与拼帧开销
type discardWriter struct{ header http.Header }
func (d *discardWriter) Header() http.Header {
if d.header == nil {
d.header = http.Header{}
}
return d.header
}
func (d *discardWriter) WriteHeader(int) {}
func (d *discardWriter) Write(p []byte) (int, error) { return len(p), nil }
// BenchmarkSSEEvent_MarshalFprintf 旧路径:json.Marshal → string() → fmt.Fprintf
func BenchmarkSSEEvent_MarshalFprintf(b *testing.B) {
w := &discardWriter{}
payload := benchClaudeDelta()
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
data, err := json.Marshal(payload)
if err != nil {
b.Fatal(err)
}
if err := WriteSSEEvent(w, "content_block_delta", string(data)); err != nil {
b.Fatal(err)
}
}
}
// BenchmarkSSEEvent_PooledJSON 新路径:池化 buffer + 绑定的 json.Encoder,整帧单次 Write
func BenchmarkSSEEvent_PooledJSON(b *testing.B) {
w := &discardWriter{}
payload := benchClaudeDelta()
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
if err := WriteSSEEventJSON(w, "content_block_delta", payload); err != nil {
b.Fatal(err)
}
}
}
// BenchmarkSSEEvent_MarshalFprintfParallel 并发下的旧路径(真实场景是多请求同时流式输出)
func BenchmarkSSEEvent_MarshalFprintfParallel(b *testing.B) {
payload := benchClaudeDelta()
b.ReportAllocs()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
w := &discardWriter{}
for pb.Next() {
data, err := json.Marshal(payload)
if err != nil {
b.Fatal(err)
}
if err := WriteSSEEvent(w, "content_block_delta", string(data)); err != nil {
b.Fatal(err)
}
}
})
}
// BenchmarkSSEEvent_PooledJSONParallel 并发下的新路径,验证 sync.Pool 的 per-P 缓存不成为瓶颈
func BenchmarkSSEEvent_PooledJSONParallel(b *testing.B) {
payload := benchClaudeDelta()
b.ReportAllocs()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
w := &discardWriter{}
for pb.Next() {
if err := WriteSSEEventJSON(w, "content_block_delta", payload); err != nil {
b.Fatal(err)
}
}
})
}
// concurrentProbe 并发独立性探针载荷:stream/seq 定位来源,pad 让各流的帧长度不同,
// 一旦缓冲被跨流复用或 Reset 失效,长度或字段就会对不上。
type concurrentProbe struct {
Stream int `json:"stream"`
Seq int `json:"seq"`
Pad string `json:"pad"`
}
// TestWriteSSEEventJSON_ConcurrentIndependence 100 条流并发输出时,池化缓冲必须完全独占:
// 每条流的响应里只能有自己的事件,顺序、内容、长度都不得被别的流污染。
//
// sync.Pool 的 Get 会把对象从池中取走,两个并发的 Get 不可能拿到同一个对象;
// 本用例锁死这一点,防止将来有人把 eb 或 eb.buf.Bytes() 的引用泄漏到 Put 之后。
func TestWriteSSEEventJSON_ConcurrentIndependence(t *testing.T) {
const streams = 100
const eventsPerStream = 50
var wg sync.WaitGroup
bodies := make([]string, streams)
errs := make([]error, streams)
for s := 0; s < streams; s++ {
wg.Add(1)
go func(s int) {
defer wg.Done()
// 每条流一个独立 writer,对应真实场景里每个请求各自的 ResponseWriter
rec := httptest.NewRecorder()
w := NewSafeWriter(rec)
for e := 0; e < eventsPerStream; e++ {
payload := concurrentProbe{Stream: s, Seq: e, Pad: strings.Repeat("x", s%64)}
if err := WriteSSEEventJSON(w, "content_block_delta", payload); err != nil {
errs[s] = err
return
}
}
bodies[s] = rec.Body.String()
}(s)
}
wg.Wait()
for s := 0; s < streams; s++ {
if errs[s] != nil {
t.Fatalf("stream %d 写入失败: %v", s, errs[s])
}
events := decodeSSE(bodies[s])
if len(events) != eventsPerStream {
t.Fatalf("stream %d 事件数 = %d, want %d", s, len(events), eventsPerStream)
}
for e, ev := range events {
var got concurrentProbe
if err := json.Unmarshal([]byte(ev.data), &got); err != nil {
t.Fatalf("stream %d 事件 #%d 不是合法 JSON(缓冲被跨流复用会产生这种拼接残片): %v\ndata=%q",
s, e, err, ev.data)
}
if got.Stream != s || got.Seq != e || len(got.Pad) != s%64 {
t.Fatalf("stream %d 事件 #%d 内容串流: got %+v, want stream=%d seq=%d padLen=%d",
s, e, got, s, e, s%64)
}
}
}
}
// TestWriteSSEEventJSON_ConcurrentWithPing 并发输出叠加保活 ping:
// 帧在私有缓冲里拼好后单次写出,ping 只能落在帧边界,客户端不会拿到空 data 事件。
func TestWriteSSEEventJSON_ConcurrentWithPing(t *testing.T) {
const streams = 32
const eventsPerStream = 30
var wg sync.WaitGroup
bodies := make([]string, streams)
for s := 0; s < streams; s++ {
wg.Add(1)
go func(s int) {
defer wg.Done()
rec := httptest.NewRecorder()
w := NewSafeWriter(rec)
stopPing := make(chan struct{})
pingDone := make(chan struct{})
go func() {
defer close(pingDone)
for {
select {
case <-stopPing:
return
default:
_ = WriteSSEPing(w)
}
}
}()
for e := 0; e < eventsPerStream; e++ {
_ = WriteSSEEventJSON(w, "content_block_delta", concurrentProbe{Stream: s, Seq: e})
}
close(stopPing)
<-pingDone
bodies[s] = rec.Body.String()
}(s)
}
wg.Wait()
for s := 0; s < streams; s++ {
assertNoEmptyDataEvent(t, bodies[s])
}
}
// TestWriteSSEEventJSON_OversizedPayload 单个事件远超池缓冲的初始容量与回收阈值时,
// 必须照常完整写出(bytes.Buffer 自动扩容,不截断、不报错);超阈值的缓冲不回池,
// 但不影响后续事件的正确性。
func TestWriteSSEEventJSON_OversizedPayload(t *testing.T) {
// 1 MiB 文本,远超 sseEventBufInitCap(2KiB) 与 sseEventBufKeepCap(64KiB)
huge := strings.Repeat("你好", 512*1024/6)
payload := concurrentProbe{Stream: 1, Seq: 2, Pad: huge}
rec := httptest.NewRecorder()
if err := WriteSSEEventJSON(rec, "content_block_delta", payload); err != nil {
t.Fatalf("超大事件写出失败: %v", err)
}
// 与旧路径逐字节一致(即:既没截断也没串位)
want := httptest.NewRecorder()
data, _ := json.Marshal(payload)
_ = WriteSSEEvent(want, "content_block_delta", string(data))
if rec.Body.String() != want.Body.String() {
t.Fatalf("超大事件输出与 Marshal 路径不一致(len got=%d want=%d)",
rec.Body.Len(), want.Body.Len())
}
// 超阈值缓冲被丢弃后,后续事件仍然正确(下一次 Get 会新建)
next := httptest.NewRecorder()
if err := WriteSSEEventJSON(next, "content_block_delta", concurrentProbe{Stream: 3, Seq: 4}); err != nil {
t.Fatalf("后续事件写出失败: %v", err)
}
events := decodeSSE(next.Body.String())
if len(events) != 1 {
t.Fatalf("后续事件数 = %d, want 1", len(events))
}
var got concurrentProbe
if err := json.Unmarshal([]byte(events[0].data), &got); err != nil {
t.Fatalf("后续事件不是合法 JSON: %v", err)
}
if got.Stream != 3 || got.Seq != 4 || got.Pad != "" {
t.Errorf("后续事件被超大缓冲污染: %+v", got)
}
}
// TestWriteSSEEventJSON_RealWorldFrameSizes 记录真实流式事件的实际帧长度,
// 用于校准 sseEventBufInitCap(初始容量应覆盖绝大多数事件,避免首次写入扩容)。
func TestWriteSSEEventJSON_RealWorldFrameSizes(t *testing.T) {
text := "这是一段典型长度的流式增量文本内容"
idx := 0
cases := []struct {
name string
event string
payload any
}{
{"content_block_delta(文本)", "content_block_delta", &dto.ClaudeResponse{
Type: "content_block_delta", Index: &idx,
Delta: &dto.ClaudeDelta{Type: "text_delta", Text: &text},
}},
{"message_start", "message_start", &dto.ClaudeResponse{
Type: "message_start",
Message: &dto.ClaudeMessageInfo{
ID: "msg_01ABCDEFGHIJKLMNOPQRSTUV", Type: "message", Role: "assistant",
Model: "claude-opus-4-8", Content: []dto.ClaudeContentBlock{},
Usage: &dto.ClaudeUsage{InputTokens: 1234, OutputTokens: 0, CacheReadInputTokens: 5678},
},
}},
{"message_delta(含 usage)", "message_delta", &dto.ClaudeResponse{
Type: "message_delta",
Delta: &dto.ClaudeDelta{StopReason: &text},
Usage: &dto.ClaudeUsage{InputTokens: 1234, OutputTokens: 567},
}},
{"message_stop", "message_stop", &dto.ClaudeResponse{Type: "message_stop"}},
}
for _, c := range cases {
rec := httptest.NewRecorder()
if err := WriteSSEEventJSON(rec, c.event, c.payload); err != nil {
t.Fatalf("%s 写出失败: %v", c.name, err)
}
n := rec.Body.Len()
t.Logf("%-28s 整帧 %4d 字节(初始容量 %d)", c.name, n, sseEventBufInitCap)
if n > sseEventBufInitCap {
t.Errorf("%s 帧长 %d 超过初始容量 %d,每个事件都会触发扩容,应上调 sseEventBufInitCap",
c.name, n, sseEventBufInitCap)
}
}
}
// BenchmarkSSEData_PooledJSON_Pointer / _Value 验证「传指针而非结构体值」的理由:
// 结构体值传进 any 形参需要装箱,会额外堆分配一份拷贝;传指针则不会。
// 迁移调用点时据此统一传 &chunk。
func BenchmarkSSEData_PooledJSON_Pointer(b *testing.B) {
w := &discardWriter{}
payload := benchClaudeDelta() // 已是指针
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
if err := WriteSSEDataJSON(w, payload); err != nil {
b.Fatal(err)
}
}
}
func BenchmarkSSEData_PooledJSON_Value(b *testing.B) {
w := &discardWriter{}
payload := *benchClaudeDelta() // 解引用成值
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
if err := WriteSSEDataJSON(w, payload); err != nil {
b.Fatal(err)
}
}
}
// TestSSEFrameWriter_MaxBufDegradeSuppressesPing 超大帧降级为分次写出时,写出的这批字节
// 不含帧尾 —— 此刻放行 ping 依然会把帧劈成两个事件。降级期间必须抑制 ping,直到分隔空行到达。
func TestSSEFrameWriter_MaxBufDegradeSuppressesPing(t *testing.T) {
rec := httptest.NewRecorder()
sw := NewSafeWriter(rec)
frame := NewSSEFrameWriter(sw)
if err := frame.WriteLine("event: content_block_delta\n"); err != nil {
t.Fatalf("WriteLine failed: %v", err)
}
// 帧尚未降级,ping 可以正常写(半帧还在缓冲里,没有出网风险)
if written, err := sw.WritePing(); err != nil || !written {
t.Fatalf("未降级时 ping 应正常写出: written=%v err=%v", written, err)
}
line := "data: " + strings.Repeat("x", 64*1024) + "\n"
for i := 0; i < sseFrameMaxBuf/len(line)+2; i++ {
if err := frame.WriteLine(line); err != nil {
t.Fatalf("WriteLine failed: %v", err)
}
}
if rec.Body.Len() == 0 {
t.Fatal("超过上限后仍未降级写出,无法验证抑制逻辑")
}
// 降级已发生:帧的一部分已经出网且没有帧尾,此刻 ping 必须让路
written, err := sw.WritePing()
if err != nil {
t.Fatalf("WritePing failed: %v", err)
}
if written {
t.Error("超大帧降级写出期间 ping 未被抑制,空行会把帧劈成两个事件")
}
// 帧结束后恢复:分隔空行到达,ping 重新放行
if err := frame.WriteLine("\n"); err != nil {
t.Fatalf("WriteLine failed: %v", err)
}
written, err = sw.WritePing()
if err != nil {
t.Fatalf("WritePing failed: %v", err)
}
if !written {
t.Error("帧已完整写出,ping 应恢复")
}
assertNoEmptyDataEvent(t, rec.Body.String())
}
// TestSSEFrameWriter_DiscardClearsPingSuppression 降级后走中断路径(Discard)时,
// 必须解除 ping 抑制,否则标记会永久留在 writer 上、后续保活全部失效。
func TestSSEFrameWriter_DiscardClearsPingSuppression(t *testing.T) {
rec := httptest.NewRecorder()
sw := NewSafeWriter(rec)
frame := NewSSEFrameWriter(sw)
line := "data: " + strings.Repeat("x", 64*1024) + "\n"
for i := 0; i < sseFrameMaxBuf/len(line)+2; i++ {
if err := frame.WriteLine(line); err != nil {
t.Fatalf("WriteLine failed: %v", err)
}
}
if written, _ := sw.WritePing(); written {
t.Fatal("前置条件不成立:降级期间 ping 应被抑制")
}
frame.Discard()
if written, err := sw.WritePing(); err != nil || !written {
t.Errorf("Discard 后 ping 抑制未解除: written=%v err=%v", written, err)
}
}
// TestSSEFrameWriter_WriteErrorPropagates 写客户端失败时错误必须上报,
// 供调用方走流中断结算(而非静默丢数据按成功结算)。
func TestSSEFrameWriter_WriteErrorPropagates(t *testing.T) {
fw := &failingWriter{err: errors.New("connection reset by peer")}
frame := NewSSEFrameWriter(fw)
if err := frame.WriteLine("event: x\n"); err != nil {
t.Fatalf("半帧不触发写入,不应报错: %v", err)
}
err := frame.WriteLine("\n")
if err == nil || !strings.Contains(err.Error(), "connection reset") {
t.Fatalf("WriteLine 帧结束时错误未上报: %v", err)
}
// 失败后缓冲已清空,不会重复写出
if frame.Buffered() != 0 {
t.Errorf("写失败后 Buffered() = %d, want 0", frame.Buffered())
}
}
// failingWriter 写入必定失败的 ResponseWriter,用于验证错误传播
type failingWriter struct {
err error
}
func (f *failingWriter) Header() http.Header { return http.Header{} }
func (f *failingWriter) WriteHeader(int) {}
func (f *failingWriter) Write([]byte) (int, error) { return 0, f.err }
func TestExtractSSEData(t *testing.T) {
tests := []struct {
name string
line string
wantData string
wantOk bool
}{
{
name: "标准格式(带空格)",
line: "data: Hello World",
wantData: "Hello World",
wantOk: true,
},
{
name: "标准格式(不带空格)",
line: "data:HelloWorld",
wantData: "HelloWorld",
wantOk: true,
},
{
name: "JSON 数据",
line: `data: {"message":"test"}`,
wantData: `{"message":"test"}`,
wantOk: true,
},
{
name: "非 data 行",
line: "event: message",
wantData: "",
wantOk: false,
},
{
name: "注释行",
line: ": PING",
wantData: "",
wantOk: false,
},
{
name: "空 data",
line: "data: ",
wantData: "",
wantOk: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
data, ok := ExtractSSEData(tt.line)
if ok != tt.wantOk {
t.Errorf("ExtractSSEData() ok = %v, want %v", ok, tt.wantOk)
}
if data != tt.wantData {
t.Errorf("ExtractSSEData() data = %q, want %q", data, tt.wantData)
}
})
}
}