mirror of
https://github.com/CJackHwang/ds2api.git
synced 2026-05-05 00:45:29 +08:00
Core changes: - stream.go: New accumulation buffer architecture with scanner goroutine + select loop, MinChars=16, MaxWait=10ms, first-flush-immediate - dedupe.go: Add TrimContinuationOverlapFromBuilder to avoid string copies - claude/stream_runtime_core.go: Integrate toolstream for incremental text - claude/stream_runtime_finalize.go: toolstream flush support - stream_emitter.js: Reduce DeltaCoalescer thresholds (160->16 chars, 80->20ms) - empty_retry: Add thinking-aware empty output detection - Fix reasoning_content leak and finish_reason=null in edge cases - Fix tail content truncation when max_tokens exceeded Tests: sync test expectations with upstream for thinking content
259 lines
6.4 KiB
Go
259 lines
6.4 KiB
Go
package sse
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestStartParsedLinePumpEmptyBody(t *testing.T) {
|
|
body := strings.NewReader("")
|
|
results, done := StartParsedLinePump(context.Background(), body, false, "text")
|
|
|
|
collected := make([]LineResult, 0)
|
|
for r := range results {
|
|
collected = append(collected, r)
|
|
}
|
|
if err := <-done; err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if len(collected) != 0 {
|
|
t.Fatalf("expected no results for empty body, got %d", len(collected))
|
|
}
|
|
}
|
|
|
|
func TestStartParsedLinePumpMultipleLines(t *testing.T) {
|
|
body := strings.NewReader(
|
|
"data: {\"p\":\"response/thinking_content\",\"v\":\"think\"}\n" +
|
|
"data: {\"p\":\"response/content\",\"v\":\"text\"}\n" +
|
|
"data: [DONE]\n",
|
|
)
|
|
results, done := StartParsedLinePump(context.Background(), body, true, "thinking")
|
|
|
|
collected := make([]LineResult, 0)
|
|
for r := range results {
|
|
collected = append(collected, r)
|
|
}
|
|
if err := <-done; err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if len(collected) < 2 {
|
|
t.Fatalf("expected at least 2 results, got %d", len(collected))
|
|
}
|
|
hasThinking := false
|
|
for _, r := range collected {
|
|
for _, p := range r.Parts {
|
|
if p.Type == "thinking" {
|
|
hasThinking = true
|
|
}
|
|
}
|
|
}
|
|
if !hasThinking {
|
|
t.Fatal("expected thinking part in results")
|
|
}
|
|
last := collected[len(collected)-1]
|
|
if !last.Stop {
|
|
t.Fatal("expected last result to be stop")
|
|
}
|
|
}
|
|
|
|
func TestStartParsedLinePumpTypeTracking(t *testing.T) {
|
|
body := strings.NewReader(
|
|
"data: {\"p\":\"response/fragments\",\"o\":\"APPEND\",\"v\":[{\"type\":\"THINK\",\"content\":\"思\"}]}\n" +
|
|
"data: {\"p\":\"response/fragments/-1/content\",\"v\":\"考\"}\n" +
|
|
"data: {\"p\":\"response/fragments\",\"o\":\"APPEND\",\"v\":[{\"type\":\"RESPONSE\",\"content\":\"答\"}]}\n" +
|
|
"data: {\"p\":\"response/fragments/-1/content\",\"v\":\"案\"}\n" +
|
|
"data: [DONE]\n",
|
|
)
|
|
results, done := StartParsedLinePump(context.Background(), body, true, "text")
|
|
|
|
types := make([]string, 0)
|
|
for r := range results {
|
|
for _, p := range r.Parts {
|
|
types = append(types, p.Type)
|
|
}
|
|
}
|
|
<-done
|
|
|
|
if len(types) == 0 {
|
|
t.Fatal("expected some parts, got none")
|
|
}
|
|
hasThinking := false
|
|
hasText := false
|
|
for _, tp := range types {
|
|
if tp == "thinking" {
|
|
hasThinking = true
|
|
}
|
|
if tp == "text" {
|
|
hasText = true
|
|
}
|
|
}
|
|
if !hasThinking {
|
|
t.Fatalf("expected thinking type in results, got %v", types)
|
|
}
|
|
if !hasText {
|
|
t.Fatalf("expected text type in results, got %v", types)
|
|
}
|
|
}
|
|
|
|
func TestStartParsedLinePumpContextCancellation(t *testing.T) {
|
|
pr, pw := io.Pipe()
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
results, done := StartParsedLinePump(ctx, pr, false, "text")
|
|
|
|
go func() {
|
|
_, _ = io.WriteString(pw, "data: {\"p\":\"response/content\",\"v\":\"hello\"}\n")
|
|
time.Sleep(50 * time.Millisecond)
|
|
_ = pw.Close()
|
|
}()
|
|
|
|
r := <-results
|
|
if !r.Parsed || len(r.Parts) == 0 {
|
|
t.Fatalf("expected first parsed result, got %#v", r)
|
|
}
|
|
|
|
cancel()
|
|
|
|
for range results {
|
|
}
|
|
|
|
err := <-done
|
|
if err != nil && err != context.Canceled {
|
|
t.Fatalf("expected context.Canceled or nil error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestStartParsedLinePumpOnlyDONE(t *testing.T) {
|
|
body := strings.NewReader("data: [DONE]\n")
|
|
results, done := StartParsedLinePump(context.Background(), body, false, "text")
|
|
|
|
collected := make([]LineResult, 0)
|
|
for r := range results {
|
|
collected = append(collected, r)
|
|
}
|
|
<-done
|
|
|
|
if len(collected) != 1 {
|
|
t.Fatalf("expected 1 result, got %d", len(collected))
|
|
}
|
|
if !collected[0].Stop {
|
|
t.Fatal("expected stop on [DONE]")
|
|
}
|
|
}
|
|
|
|
func TestStartParsedLinePumpNonSSELines(t *testing.T) {
|
|
body := strings.NewReader(
|
|
"event: update\n" +
|
|
": comment line\n" +
|
|
"data: {\"p\":\"response/content\",\"v\":\"valid\"}\n" +
|
|
"data: [DONE]\n",
|
|
)
|
|
results, done := StartParsedLinePump(context.Background(), body, false, "text")
|
|
|
|
var validCount int
|
|
for r := range results {
|
|
if r.Parsed && len(r.Parts) > 0 {
|
|
validCount++
|
|
}
|
|
}
|
|
<-done
|
|
|
|
if validCount != 1 {
|
|
t.Fatalf("expected 1 valid result, got %d", validCount)
|
|
}
|
|
}
|
|
|
|
func TestStartParsedLinePumpThinkingDisabled(t *testing.T) {
|
|
body := strings.NewReader(
|
|
"data: {\"p\":\"response/fragments\",\"o\":\"APPEND\",\"v\":[{\"type\":\"THINK\",\"content\":\"思\"}]}\n" +
|
|
"data: {\"p\":\"response/fragments/-1/content\",\"v\":\"考\"}\n" +
|
|
"data: {\"v\":\"隐藏\"}\n" +
|
|
"data: {\"p\":\"response/fragments\",\"o\":\"APPEND\",\"v\":[{\"type\":\"RESPONSE\",\"content\":\"答\"}]}\n" +
|
|
"data: {\"p\":\"response/content\",\"v\":\"response\"}\n" +
|
|
"data: [DONE]\n",
|
|
)
|
|
results, done := StartParsedLinePump(context.Background(), body, false, "text")
|
|
|
|
var parts []ContentPart
|
|
for r := range results {
|
|
parts = append(parts, r.Parts...)
|
|
}
|
|
<-done
|
|
|
|
got := strings.Builder{}
|
|
for _, p := range parts {
|
|
if p.Type != "text" {
|
|
t.Fatalf("expected only text parts with thinking disabled, got %#v", parts)
|
|
}
|
|
got.WriteString(p.Text)
|
|
}
|
|
if got.String() != "答response" {
|
|
t.Fatalf("expected hidden thinking to be dropped, got %q from %#v", got.String(), parts)
|
|
}
|
|
}
|
|
|
|
func TestStartParsedLinePumpAccumulatesSmallChunks(t *testing.T) {
|
|
body := strings.NewReader(
|
|
"data: {\"p\":\"response/content\",\"v\":\"h\"}\n" +
|
|
"data: {\"p\":\"response/content\",\"v\":\"i\"}\n" +
|
|
"data: [DONE]\n",
|
|
)
|
|
|
|
results, done := StartParsedLinePump(context.Background(), body, false, "text")
|
|
|
|
collected := make([]LineResult, 0)
|
|
for r := range results {
|
|
collected = append(collected, r)
|
|
}
|
|
if err := <-done; err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
last := collected[len(collected)-1]
|
|
if !last.Stop {
|
|
t.Fatal("expected last result to stop")
|
|
}
|
|
|
|
allText := strings.Builder{}
|
|
for _, r := range collected {
|
|
for _, p := range r.Parts {
|
|
allText.WriteString(p.Text)
|
|
}
|
|
}
|
|
if allText.String() != "hi" {
|
|
t.Fatalf("expected accumulated text 'hi', got %q", allText.String())
|
|
}
|
|
}
|
|
|
|
func TestStartParsedLinePumpFirstFlushImmediate(t *testing.T) {
|
|
body := strings.NewReader(
|
|
"data: {\"p\":\"response/content\",\"v\":\"Hi\"}\n" +
|
|
"data: [DONE]\n",
|
|
)
|
|
|
|
results, done := StartParsedLinePump(context.Background(), body, false, "text")
|
|
|
|
collected := make([]LineResult, 0)
|
|
for r := range results {
|
|
collected = append(collected, r)
|
|
}
|
|
if err := <-done; err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
hasContent := false
|
|
for _, r := range collected {
|
|
for _, p := range r.Parts {
|
|
if p.Text == "Hi" {
|
|
hasContent = true
|
|
}
|
|
}
|
|
}
|
|
if !hasContent {
|
|
t.Fatal("expected 'Hi' content in results")
|
|
}
|
|
}
|