test: Introduce comprehensive edge case tests for various internal packages including SSE, Claude, Auth, Account, Config, Deepseek, Admin, and Util.

This commit is contained in:
CJACK
2026-02-18 16:52:16 +08:00
parent deec72416e
commit f2b10992cc
14 changed files with 3291 additions and 7 deletions

View File

@@ -0,0 +1,140 @@
package sse
import (
"io"
"net/http"
"strings"
"testing"
)
// ─── CollectStream edge cases ────────────────────────────────────────
func makeHTTPResponse(body string) *http.Response {
return &http.Response{
StatusCode: http.StatusOK,
Header: make(http.Header),
Body: io.NopCloser(strings.NewReader(body)),
}
}
func TestCollectStreamEmpty(t *testing.T) {
resp := makeHTTPResponse("")
result := CollectStream(resp, false, false)
if result.Text != "" || result.Thinking != "" {
t.Fatalf("expected empty result, got text=%q think=%q", result.Text, result.Thinking)
}
}
func TestCollectStreamTextOnly(t *testing.T) {
resp := makeHTTPResponse(
"data: {\"p\":\"response/content\",\"v\":\"Hello\"}\n" +
"data: {\"p\":\"response/content\",\"v\":\" World\"}\n" +
"data: [DONE]\n",
)
result := CollectStream(resp, false, false)
if result.Text != "Hello World" {
t.Fatalf("expected 'Hello World', got %q", result.Text)
}
if result.Thinking != "" {
t.Fatalf("expected no thinking, got %q", result.Thinking)
}
}
func TestCollectStreamThinkingAndText(t *testing.T) {
resp := makeHTTPResponse(
"data: {\"p\":\"response/thinking_content\",\"v\":\"Thinking...\"}\n" +
"data: {\"p\":\"response/content\",\"v\":\"Answer\"}\n" +
"data: [DONE]\n",
)
result := CollectStream(resp, true, true)
if result.Thinking != "Thinking..." {
t.Fatalf("expected 'Thinking...', got %q", result.Thinking)
}
if result.Text != "Answer" {
t.Fatalf("expected 'Answer', got %q", result.Text)
}
}
func TestCollectStreamOnlyThinking(t *testing.T) {
resp := makeHTTPResponse(
"data: {\"p\":\"response/thinking_content\",\"v\":\"Only thinking\"}\n" +
"data: [DONE]\n",
)
result := CollectStream(resp, true, true)
if result.Thinking != "Only thinking" {
t.Fatalf("expected 'Only thinking', got %q", result.Thinking)
}
if result.Text != "" {
t.Fatalf("expected empty text, got %q", result.Text)
}
}
func TestCollectStreamSkipsInvalidLines(t *testing.T) {
resp := makeHTTPResponse(
"event: comment\n" +
"data: invalid_json\n" +
"data: {\"p\":\"response/content\",\"v\":\"valid\"}\n" +
"data: [DONE]\n",
)
result := CollectStream(resp, false, false)
if result.Text != "valid" {
t.Fatalf("expected 'valid', got %q", result.Text)
}
}
func TestCollectStreamWithFragments(t *testing.T) {
resp := makeHTTPResponse(
"data: {\"p\":\"response/fragments\",\"o\":\"APPEND\",\"v\":[{\"type\":\"THINK\",\"content\":\"Think\"}]}\n" +
"data: {\"p\":\"response/fragments\",\"o\":\"APPEND\",\"v\":[{\"type\":\"RESPONSE\",\"content\":\"Done\"}]}\n" +
"data: [DONE]\n",
)
result := CollectStream(resp, true, true)
if result.Thinking != "Think" {
t.Fatalf("expected 'Think' thinking, got %q", result.Thinking)
}
if result.Text != "Done" {
t.Fatalf("expected 'Done' text, got %q", result.Text)
}
}
func TestCollectStreamWithCitation(t *testing.T) {
resp := makeHTTPResponse(
"data: {\"p\":\"response/content\",\"v\":\"Hello\"}\n" +
"data: {\"p\":\"response/content\",\"v\":\"[citation:1] cited text\"}\n" +
"data: {\"p\":\"response/content\",\"v\":\" more\"}\n" +
"data: [DONE]\n",
)
result := CollectStream(resp, false, false)
// CollectStream does NOT filter citations (that's done by the adapters)
// So citations are passed through as-is
if !strings.Contains(result.Text, "[citation:1]") {
t.Fatalf("expected citations to be passed through, got %q", result.Text)
}
if result.Text != "Hello[citation:1] cited text more" {
t.Fatalf("expected full text with citation, got %q", result.Text)
}
}
func TestCollectStreamMultipleThinkingChunks(t *testing.T) {
resp := makeHTTPResponse(
"data: {\"p\":\"response/thinking_content\",\"v\":\"part1\"}\n" +
"data: {\"p\":\"response/thinking_content\",\"v\":\" part2\"}\n" +
"data: {\"p\":\"response/content\",\"v\":\"answer\"}\n" +
"data: [DONE]\n",
)
result := CollectStream(resp, true, true)
if result.Thinking != "part1 part2" {
t.Fatalf("expected 'part1 part2', got %q", result.Thinking)
}
}
func TestCollectStreamStatusFinished(t *testing.T) {
resp := makeHTTPResponse(
"data: {\"p\":\"response/content\",\"v\":\"Hello\"}\n" +
"data: {\"p\":\"response/status\",\"v\":\"FINISHED\"}\n",
)
result := CollectStream(resp, false, false)
if result.Text != "Hello" {
t.Fatalf("expected 'Hello', got %q", result.Text)
}
}

View File

@@ -0,0 +1,70 @@
package sse
import "testing"
func TestParseDeepSeekContentLineNotParsed(t *testing.T) {
res := ParseDeepSeekContentLine([]byte("not a data line"), false, "text")
if res.Parsed {
t.Fatal("expected not parsed")
}
if res.NextType != "text" {
t.Fatalf("expected nextType preserved, got %q", res.NextType)
}
}
func TestParseDeepSeekContentLinePreservesNextType(t *testing.T) {
res := ParseDeepSeekContentLine([]byte(`data: {"p":"response/thinking_content","v":"think"}`), true, "thinking")
if !res.Parsed || res.Stop {
t.Fatalf("expected parsed non-stop: %#v", res)
}
if len(res.Parts) != 1 || res.Parts[0].Type != "thinking" {
t.Fatalf("unexpected parts: %#v", res.Parts)
}
}
func TestParseDeepSeekContentLineFragmentSwitchType(t *testing.T) {
res := ParseDeepSeekContentLine(
[]byte(`data: {"p":"response/fragments","o":"APPEND","v":[{"type":"RESPONSE","content":"hi"}]}`),
true, "thinking",
)
if !res.Parsed || res.Stop {
t.Fatalf("expected parsed non-stop: %#v", res)
}
if res.NextType != "text" {
t.Fatalf("expected nextType text after RESPONSE fragment, got %q", res.NextType)
}
}
func TestParseDeepSeekContentLineContentFilterMessage(t *testing.T) {
res := ParseDeepSeekContentLine([]byte(`data: {"code":"content_filter"}`), false, "text")
if !res.ContentFilter {
t.Fatal("expected content filter flag")
}
if res.ErrorMessage == "" {
t.Fatal("expected error message on content filter")
}
}
func TestParseDeepSeekContentLineErrorObjectFormat(t *testing.T) {
res := ParseDeepSeekContentLine([]byte(`data: {"error":{"message":"rate limit","code":429}}`), false, "text")
if !res.Parsed || !res.Stop {
t.Fatalf("expected parsed stop: %#v", res)
}
if res.ErrorMessage == "" {
t.Fatal("expected non-empty error message")
}
}
func TestParseDeepSeekContentLineInvalidJSON(t *testing.T) {
res := ParseDeepSeekContentLine([]byte("data: {broken"), false, "text")
if res.Parsed {
t.Fatal("expected not parsed for broken JSON")
}
}
func TestParseDeepSeekContentLineEmptyBytes(t *testing.T) {
res := ParseDeepSeekContentLine([]byte{}, false, "text")
if res.Parsed {
t.Fatal("expected not parsed for empty bytes")
}
}

View File

@@ -0,0 +1,631 @@
package sse
import "testing"
// ─── ParseDeepSeekSSELine edge cases ─────────────────────────────────
func TestParseDeepSeekSSELineEmptyLine(t *testing.T) {
_, _, ok := ParseDeepSeekSSELine([]byte(""))
if ok {
t.Fatal("expected not parsed for empty line")
}
}
func TestParseDeepSeekSSELineNoDataPrefix(t *testing.T) {
_, _, ok := ParseDeepSeekSSELine([]byte("event: message"))
if ok {
t.Fatal("expected not parsed for non-data line")
}
}
func TestParseDeepSeekSSELineInvalidJSON(t *testing.T) {
_, _, ok := ParseDeepSeekSSELine([]byte("data: {invalid json"))
if ok {
t.Fatal("expected not parsed for invalid JSON")
}
}
func TestParseDeepSeekSSELineWhitespaceOnly(t *testing.T) {
_, _, ok := ParseDeepSeekSSELine([]byte(" "))
if ok {
t.Fatal("expected not parsed for whitespace-only line")
}
}
func TestParseDeepSeekSSELineDataWithExtraSpaces(t *testing.T) {
chunk, done, ok := ParseDeepSeekSSELine([]byte(`data: {"v":"hello"} `))
if !ok || done {
t.Fatalf("expected parsed chunk for spaced data line")
}
if chunk["v"] != "hello" {
t.Fatalf("unexpected chunk: %#v", chunk)
}
}
// ─── shouldSkipPath edge cases ───────────────────────────────────────
func TestShouldSkipPathQuasiStatus(t *testing.T) {
if !shouldSkipPath("response/quasi_status") {
t.Fatal("expected skip for quasi_status path")
}
}
func TestShouldSkipPathElapsedSecs(t *testing.T) {
if !shouldSkipPath("response/elapsed_secs") {
t.Fatal("expected skip for elapsed_secs path")
}
}
func TestShouldSkipPathTokenUsage(t *testing.T) {
if !shouldSkipPath("response/token_usage") {
t.Fatal("expected skip for token_usage path")
}
}
func TestShouldSkipPathPendingFragment(t *testing.T) {
if !shouldSkipPath("response/pending_fragment") {
t.Fatal("expected skip for pending_fragment path")
}
}
func TestShouldSkipPathConversationMode(t *testing.T) {
if !shouldSkipPath("response/conversation_mode") {
t.Fatal("expected skip for conversation_mode path")
}
}
func TestShouldSkipPathSearchStatus(t *testing.T) {
if !shouldSkipPath("response/search_status") {
t.Fatal("expected skip for search_status path")
}
}
func TestShouldSkipPathFragmentStatus(t *testing.T) {
if !shouldSkipPath("response/fragments/-1/status") {
t.Fatal("expected skip for fragment -1 status")
}
if !shouldSkipPath("response/fragments/-2/status") {
t.Fatal("expected skip for fragment -2 status")
}
if !shouldSkipPath("response/fragments/-3/status") {
t.Fatal("expected skip for fragment -3 status")
}
}
func TestShouldSkipPathRegularContent(t *testing.T) {
if shouldSkipPath("response/content") {
t.Fatal("expected not skip for content path")
}
if shouldSkipPath("response/thinking_content") {
t.Fatal("expected not skip for thinking_content path")
}
}
// ─── ParseSSEChunkForContent edge cases ──────────────────────────────
func TestParseSSEChunkForContentNoVField(t *testing.T) {
parts, finished, nextType := ParseSSEChunkForContent(map[string]any{"p": "response/content"}, false, "text")
if finished {
t.Fatal("expected not finished")
}
if len(parts) != 0 {
t.Fatalf("expected no parts when v is missing, got %#v", parts)
}
if nextType != "text" {
t.Fatalf("expected type preserved, got %q", nextType)
}
}
func TestParseSSEChunkForContentSkippedPath(t *testing.T) {
parts, finished, nextType := ParseSSEChunkForContent(map[string]any{
"p": "response/token_usage",
"v": "some data",
}, false, "text")
if finished || len(parts) > 0 {
t.Fatalf("expected skipped path to produce no output")
}
if nextType != "text" {
t.Fatalf("expected type preserved for skipped path")
}
}
func TestParseSSEChunkForContentFinishedStatus(t *testing.T) {
parts, finished, _ := ParseSSEChunkForContent(map[string]any{
"p": "response/status",
"v": "FINISHED",
}, false, "text")
if !finished {
t.Fatal("expected finished on status FINISHED")
}
if len(parts) != 0 {
t.Fatalf("expected no parts on finished, got %d", len(parts))
}
}
func TestParseSSEChunkForContentStatusNotFinished(t *testing.T) {
parts, finished, _ := ParseSSEChunkForContent(map[string]any{
"p": "response/status",
"v": "IN_PROGRESS",
}, false, "text")
if finished {
t.Fatal("expected not finished for non-FINISHED status")
}
if len(parts) != 1 || parts[0].Text != "IN_PROGRESS" {
t.Fatalf("expected content for non-FINISHED status, got %#v", parts)
}
}
func TestParseSSEChunkForContentEmptyStringV(t *testing.T) {
parts, finished, _ := ParseSSEChunkForContent(map[string]any{
"p": "response/content",
"v": "",
}, false, "text")
if finished {
t.Fatal("expected not finished")
}
if len(parts) != 0 {
t.Fatalf("expected no parts for empty string v, got %#v", parts)
}
}
func TestParseSSEChunkForContentFinishedOnEmptyPath(t *testing.T) {
parts, finished, _ := ParseSSEChunkForContent(map[string]any{
"p": "",
"v": "FINISHED",
}, false, "text")
if !finished {
t.Fatal("expected finished on empty path with FINISHED value")
}
if len(parts) != 0 {
t.Fatalf("expected no parts on finished")
}
}
func TestParseSSEChunkForContentFinishedOnStatusPath(t *testing.T) {
_, finished, _ := ParseSSEChunkForContent(map[string]any{
"p": "status",
"v": "FINISHED",
}, false, "text")
if !finished {
t.Fatal("expected finished on status path with FINISHED value")
}
}
func TestParseSSEChunkForContentThinkingPathEmptyPath(t *testing.T) {
parts, _, nextType := ParseSSEChunkForContent(map[string]any{
"v": "some thought",
}, true, "thinking")
if len(parts) != 1 || parts[0].Type != "thinking" {
t.Fatalf("expected thinking part on empty path, got %#v", parts)
}
if nextType != "thinking" {
t.Fatalf("expected nextType thinking, got %q", nextType)
}
}
func TestParseSSEChunkForContentThinkingEnabledTextType(t *testing.T) {
parts, _, nextType := ParseSSEChunkForContent(map[string]any{
"v": "text content",
}, true, "text")
if len(parts) != 1 || parts[0].Type != "text" {
t.Fatalf("expected text part when currentType=text, got %#v", parts)
}
if nextType != "text" {
t.Fatalf("expected nextType text, got %q", nextType)
}
}
// ─── ParseSSEChunkForContent: fragments path with THINK type ─────────
func TestParseSSEChunkForContentFragmentsAppendThink(t *testing.T) {
chunk := map[string]any{
"p": "response/fragments",
"o": "APPEND",
"v": []any{
map[string]any{
"type": "THINK",
"content": "深入思考...",
},
},
}
parts, finished, nextType := ParseSSEChunkForContent(chunk, true, "text")
if finished {
t.Fatal("expected not finished")
}
if nextType != "thinking" {
t.Fatalf("expected nextType thinking, got %q", nextType)
}
if len(parts) != 1 || parts[0].Type != "thinking" || parts[0].Text != "深入思考..." {
t.Fatalf("unexpected parts: %#v", parts)
}
}
func TestParseSSEChunkForContentFragmentsAppendEmptyContent(t *testing.T) {
chunk := map[string]any{
"p": "response/fragments",
"o": "APPEND",
"v": []any{
map[string]any{
"type": "RESPONSE",
"content": "",
},
},
}
parts, finished, nextType := ParseSSEChunkForContent(chunk, true, "thinking")
if finished {
t.Fatal("expected not finished")
}
if nextType != "text" {
t.Fatalf("expected nextType text, got %q", nextType)
}
if len(parts) != 0 {
t.Fatalf("expected no parts for empty content, got %#v", parts)
}
}
func TestParseSSEChunkForContentFragmentsAppendDefaultType(t *testing.T) {
chunk := map[string]any{
"p": "response/fragments",
"o": "APPEND",
"v": []any{
map[string]any{
"type": "UNKNOWN",
"content": "some text",
},
},
}
parts, _, _ := ParseSSEChunkForContent(chunk, true, "text")
if len(parts) != 1 || parts[0].Type != "text" {
t.Fatalf("expected text type for unknown fragment type, got %#v", parts)
}
}
func TestParseSSEChunkForContentFragmentsAppendNonArray(t *testing.T) {
chunk := map[string]any{
"p": "response/fragments",
"o": "APPEND",
"v": "not an array",
}
parts, finished, _ := ParseSSEChunkForContent(chunk, true, "text")
if finished {
t.Fatal("expected not finished")
}
// "not an array" should be treated as string value at the end
if len(parts) != 1 || parts[0].Text != "not an array" {
t.Fatalf("unexpected parts: %#v", parts)
}
}
func TestParseSSEChunkForContentFragmentsAppendNonMap(t *testing.T) {
chunk := map[string]any{
"p": "response/fragments",
"o": "APPEND",
"v": []any{"string item"},
}
parts, _, _ := ParseSSEChunkForContent(chunk, false, "text")
// Non-map items in fragment array are skipped; the []any itself is handled later
_ = parts // just checking it doesn't panic
}
// ─── ParseSSEChunkForContent: response path with nested fragment ─────
func TestParseSSEChunkForContentResponsePathFragmentsAppend(t *testing.T) {
chunk := map[string]any{
"p": "response",
"v": []any{
map[string]any{
"p": "fragments",
"o": "APPEND",
"v": []any{
map[string]any{
"type": "THINKING",
},
},
},
},
}
_, _, nextType := ParseSSEChunkForContent(chunk, true, "text")
if nextType != "thinking" {
t.Fatalf("expected nextType thinking from response path fragments, got %q", nextType)
}
}
func TestParseSSEChunkForContentResponsePathResponseFragment(t *testing.T) {
chunk := map[string]any{
"p": "response",
"v": []any{
map[string]any{
"p": "fragments",
"o": "APPEND",
"v": []any{
map[string]any{
"type": "RESPONSE",
},
},
},
},
}
_, _, nextType := ParseSSEChunkForContent(chunk, true, "thinking")
if nextType != "text" {
t.Fatalf("expected nextType text for RESPONSE fragment, got %q", nextType)
}
}
// ─── ParseSSEChunkForContent: map value with wrapped response ────────
func TestParseSSEChunkForContentMapValueWithFragments(t *testing.T) {
chunk := map[string]any{
"v": map[string]any{
"response": map[string]any{
"fragments": []any{
map[string]any{
"type": "THINK",
"content": "思考...",
},
map[string]any{
"type": "RESPONSE",
"content": "回答...",
},
},
},
},
}
parts, finished, nextType := ParseSSEChunkForContent(chunk, true, "text")
if finished {
t.Fatal("expected not finished")
}
if nextType != "text" {
t.Fatalf("expected nextType text after RESPONSE, got %q", nextType)
}
if len(parts) != 2 {
t.Fatalf("expected 2 parts, got %d: %#v", len(parts), parts)
}
if parts[0].Type != "thinking" || parts[0].Text != "思考..." {
t.Fatalf("first part mismatch: %#v", parts[0])
}
if parts[1].Type != "text" || parts[1].Text != "回答..." {
t.Fatalf("second part mismatch: %#v", parts[1])
}
}
func TestParseSSEChunkForContentMapValueDirectFragments(t *testing.T) {
chunk := map[string]any{
"v": map[string]any{
"fragments": []any{
map[string]any{
"type": "RESPONSE",
"content": "直接回答",
},
},
},
}
parts, _, _ := ParseSSEChunkForContent(chunk, false, "text")
if len(parts) != 1 || parts[0].Text != "直接回答" || parts[0].Type != "text" {
t.Fatalf("unexpected parts for direct fragments: %#v", parts)
}
}
func TestParseSSEChunkForContentMapValueUnknownType(t *testing.T) {
chunk := map[string]any{
"v": map[string]any{
"fragments": []any{
map[string]any{
"type": "CUSTOM",
"content": "custom content",
},
},
},
}
parts, _, _ := ParseSSEChunkForContent(chunk, false, "text")
if len(parts) != 1 || parts[0].Type != "text" {
t.Fatalf("expected partType fallback for unknown type, got %#v", parts)
}
}
func TestParseSSEChunkForContentMapValueEmptyFragmentContent(t *testing.T) {
chunk := map[string]any{
"v": map[string]any{
"fragments": []any{
map[string]any{
"type": "RESPONSE",
"content": "",
},
},
},
}
parts, _, _ := ParseSSEChunkForContent(chunk, false, "text")
if len(parts) != 0 {
t.Fatalf("expected no parts for empty fragment content, got %#v", parts)
}
}
// ─── ParseSSEChunkForContent: fragments/-1/content path ──────────────
func TestParseSSEChunkForContentFragmentContentPathInheritsType(t *testing.T) {
chunk := map[string]any{
"p": "response/fragments/-1/content",
"v": "继续思考",
}
parts, _, _ := ParseSSEChunkForContent(chunk, true, "thinking")
if len(parts) != 1 || parts[0].Type != "thinking" {
t.Fatalf("expected inherited thinking type, got %#v", parts)
}
}
// ─── IsCitation edge cases ───────────────────────────────────────────
func TestIsCitationWithLeadingWhitespace(t *testing.T) {
if !IsCitation(" [citation:2] text") {
t.Fatal("expected citation true with leading whitespace")
}
}
func TestIsCitationEmpty(t *testing.T) {
if IsCitation("") {
t.Fatal("expected citation false for empty string")
}
}
func TestIsCitationSimilarPrefix(t *testing.T) {
if IsCitation("[cite:1] text") {
t.Fatal("expected citation false for [cite: prefix")
}
}
// ─── extractContentRecursive edge cases ──────────────────────────────
func TestExtractContentRecursiveFinishedStatus(t *testing.T) {
items := []any{
map[string]any{"p": "status", "v": "FINISHED"},
}
parts, finished := extractContentRecursive(items, "text")
if !finished {
t.Fatal("expected finished on status FINISHED")
}
if len(parts) != 0 {
t.Fatalf("expected no parts, got %#v", parts)
}
}
func TestExtractContentRecursiveSkipsPath(t *testing.T) {
items := []any{
map[string]any{"p": "token_usage", "v": "data"},
}
parts, finished := extractContentRecursive(items, "text")
if finished {
t.Fatal("expected not finished")
}
if len(parts) != 0 {
t.Fatalf("expected no parts for skipped path, got %#v", parts)
}
}
func TestExtractContentRecursiveContentField(t *testing.T) {
items := []any{
map[string]any{"p": "x", "v": "val", "content": "actual content", "type": "RESPONSE"},
}
parts, _ := extractContentRecursive(items, "text")
if len(parts) != 1 || parts[0].Text != "actual content" || parts[0].Type != "text" {
t.Fatalf("unexpected parts: %#v", parts)
}
}
func TestExtractContentRecursiveContentFieldThinkType(t *testing.T) {
items := []any{
map[string]any{"p": "x", "v": "val", "content": "think text", "type": "THINK"},
}
parts, _ := extractContentRecursive(items, "text")
if len(parts) != 1 || parts[0].Type != "thinking" {
t.Fatalf("expected thinking type for THINK content, got %#v", parts)
}
}
func TestExtractContentRecursiveThinkingPath(t *testing.T) {
items := []any{
map[string]any{"p": "thinking_content", "v": "deep thought"},
}
parts, _ := extractContentRecursive(items, "text")
if len(parts) != 1 || parts[0].Type != "thinking" || parts[0].Text != "deep thought" {
t.Fatalf("unexpected parts for thinking path: %#v", parts)
}
}
func TestExtractContentRecursiveContentPath(t *testing.T) {
items := []any{
map[string]any{"p": "content", "v": "text content"},
}
parts, _ := extractContentRecursive(items, "thinking")
if len(parts) != 1 || parts[0].Type != "text" {
t.Fatalf("expected text type for content path, got %#v", parts)
}
}
func TestExtractContentRecursiveResponsePath(t *testing.T) {
items := []any{
map[string]any{"p": "response", "v": "text content"},
}
parts, _ := extractContentRecursive(items, "thinking")
if len(parts) != 1 || parts[0].Type != "text" {
t.Fatalf("expected text type for response path, got %#v", parts)
}
}
func TestExtractContentRecursiveFragmentsPath(t *testing.T) {
items := []any{
map[string]any{"p": "fragments", "v": "fragment text"},
}
parts, _ := extractContentRecursive(items, "thinking")
if len(parts) != 1 || parts[0].Type != "text" {
t.Fatalf("expected text type for fragments path, got %#v", parts)
}
}
func TestExtractContentRecursiveNestedArrayWithTypes(t *testing.T) {
items := []any{
map[string]any{
"p": "fragments",
"v": []any{
map[string]any{"content": "thought", "type": "THINKING"},
map[string]any{"content": "answer", "type": "RESPONSE"},
"raw string",
},
},
}
parts, _ := extractContentRecursive(items, "text")
if len(parts) != 3 {
t.Fatalf("expected 3 parts, got %d: %#v", len(parts), parts)
}
if parts[0].Type != "thinking" || parts[0].Text != "thought" {
t.Fatalf("first part mismatch: %#v", parts[0])
}
if parts[1].Type != "text" || parts[1].Text != "answer" {
t.Fatalf("second part mismatch: %#v", parts[1])
}
if parts[2].Type != "text" || parts[2].Text != "raw string" {
t.Fatalf("third part mismatch: %#v", parts[2])
}
}
func TestExtractContentRecursiveEmptyContentSkipped(t *testing.T) {
items := []any{
map[string]any{
"p": "fragments",
"v": []any{
map[string]any{"content": "", "type": "RESPONSE"},
},
},
}
parts, _ := extractContentRecursive(items, "text")
if len(parts) != 0 {
t.Fatalf("expected no parts for empty nested content, got %#v", parts)
}
}
func TestExtractContentRecursiveFinishedString(t *testing.T) {
items := []any{
map[string]any{"p": "content", "v": "FINISHED"},
}
parts, _ := extractContentRecursive(items, "text")
// "FINISHED" string value on non-status path should be skipped
if len(parts) != 0 {
t.Fatalf("expected FINISHED string to be skipped, got %#v", parts)
}
}
func TestExtractContentRecursiveNoVField(t *testing.T) {
items := []any{
map[string]any{"p": "content"},
}
parts, _ := extractContentRecursive(items, "text")
if len(parts) != 0 {
t.Fatalf("expected no parts for missing v field, got %#v", parts)
}
}
func TestExtractContentRecursiveNonMapItem(t *testing.T) {
items := []any{"just a string", 42}
parts, _ := extractContentRecursive(items, "text")
if len(parts) != 0 {
t.Fatalf("expected no parts for non-map items, got %#v", parts)
}
}

View File

@@ -0,0 +1,177 @@
package sse
import (
"context"
"io"
"strings"
"testing"
)
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) < 3 {
t.Fatalf("expected at least 3 results, got %d", len(collected))
}
// First should be thinking
if collected[0].Parts[0].Type != "thinking" {
t.Fatalf("expected first part thinking, got %q", collected[0].Parts[0].Type)
}
// Last should be stop
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
// Should have: thinking, thinking, text, text
expected := []string{"thinking", "thinking", "text", "text"}
if len(types) != len(expected) {
t.Fatalf("expected types %v, got %v", expected, types)
}
for i, want := range expected {
if types[i] != want {
t.Fatalf("type[%d] mismatch: want %q got %q (all=%v)", i, want, types[i], types)
}
}
}
func TestStartParsedLinePumpContextCancellation(t *testing.T) {
pr, pw := io.Pipe()
ctx, cancel := context.WithCancel(context.Background())
results, done := StartParsedLinePump(ctx, pr, false, "text")
// Write one line to allow it to start
go func() {
_, _ = io.WriteString(pw, "data: {\"p\":\"response/content\",\"v\":\"hello\"}\n")
// Don't close yet - wait for context cancel
}()
// Read first result
r := <-results
if !r.Parsed || len(r.Parts) == 0 {
t.Fatalf("expected first parsed result, got %#v", r)
}
// Cancel context - this will cause the pump to exit on next send
cancel()
// Close the pipe to unblock scanner.Scan()
pw.Close()
// Drain remaining results
for range results {
}
err := <-done
// Error may be context.Canceled or nil (if pipe closed first)
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/thinking_content\",\"v\":\"thought\"}\n" +
"data: {\"p\":\"response/content\",\"v\":\"response\"}\n" +
"data: [DONE]\n",
)
// With thinking disabled, thinking content should still be emitted but marked differently
results, done := StartParsedLinePump(context.Background(), body, false, "text")
var parts []ContentPart
for r := range results {
parts = append(parts, r.Parts...)
}
<-done
if len(parts) < 1 {
t.Fatalf("expected at least 1 part, got %d", len(parts))
}
}