opencode-cursor-agent/internal/parser/stream_test.go

281 lines
6.9 KiB
Go
Raw Normal View History

2026-03-30 14:09:15 +00:00
package parser
import (
"encoding/json"
"testing"
)
func makeAssistantLine(text string) string {
obj := map[string]interface{}{
"type": "assistant",
"message": map[string]interface{}{
"content": []map[string]interface{}{
{"type": "text", "text": text},
},
},
}
b, _ := json.Marshal(obj)
return string(b)
}
func makeResultLine() string {
b, _ := json.Marshal(map[string]string{"type": "result", "subtype": "success"})
return string(b)
}
2026-04-01 00:53:34 +00:00
func TestStreamParserFragmentMode(t *testing.T) {
// cursor --stream-partial-output 模式:每個訊息是獨立 token fragment
2026-03-30 14:09:15 +00:00
var texts []string
2026-04-01 00:53:34 +00:00
p := CreateStreamParser(
2026-03-30 14:09:15 +00:00
func(text string) { texts = append(texts, text) },
2026-04-01 00:53:34 +00:00
func() {},
2026-03-30 14:09:15 +00:00
)
2026-04-01 00:53:34 +00:00
p.Parse(makeAssistantLine("你"))
p.Parse(makeAssistantLine("好!有"))
p.Parse(makeAssistantLine("什"))
p.Parse(makeAssistantLine("麼"))
2026-03-30 14:09:15 +00:00
2026-04-01 00:53:34 +00:00
if len(texts) != 4 {
t.Fatalf("expected 4 fragments, got %d: %v", len(texts), texts)
}
if texts[0] != "你" || texts[1] != "好!有" || texts[2] != "什" || texts[3] != "麼" {
t.Fatalf("unexpected texts: %v", texts)
2026-03-30 14:09:15 +00:00
}
}
2026-04-01 00:53:34 +00:00
func TestStreamParserDeduplicatesFinalFullText(t *testing.T) {
// 最後一個訊息是完整的累積文字,應被跳過(去重)
2026-03-30 14:09:15 +00:00
var texts []string
2026-04-01 00:53:34 +00:00
p := CreateStreamParser(
2026-03-30 14:09:15 +00:00
func(text string) { texts = append(texts, text) },
func() {},
)
2026-04-01 00:53:34 +00:00
p.Parse(makeAssistantLine("Hello"))
p.Parse(makeAssistantLine(" world"))
// 最後一個是完整累積文字,應被去重
p.Parse(makeAssistantLine("Hello world"))
2026-03-30 14:09:15 +00:00
if len(texts) != 2 {
2026-04-01 00:53:34 +00:00
t.Fatalf("expected 2 fragments (final full text deduplicated), got %d: %v", len(texts), texts)
2026-03-30 14:09:15 +00:00
}
2026-04-01 00:53:34 +00:00
if texts[0] != "Hello" || texts[1] != " world" {
2026-03-30 14:09:15 +00:00
t.Fatalf("unexpected texts: %v", texts)
}
}
func TestStreamParserCallsOnDone(t *testing.T) {
var texts []string
doneCount := 0
2026-04-01 00:53:34 +00:00
p := CreateStreamParser(
2026-03-30 14:09:15 +00:00
func(text string) { texts = append(texts, text) },
func() { doneCount++ },
)
2026-04-01 00:53:34 +00:00
p.Parse(makeResultLine())
2026-03-30 14:09:15 +00:00
if doneCount != 1 {
t.Fatalf("expected onDone called once, got %d", doneCount)
}
if len(texts) != 0 {
t.Fatalf("expected no text, got %v", texts)
}
}
func TestStreamParserIgnoresLinesAfterDone(t *testing.T) {
var texts []string
doneCount := 0
2026-04-01 00:53:34 +00:00
p := CreateStreamParser(
2026-03-30 14:09:15 +00:00
func(text string) { texts = append(texts, text) },
func() { doneCount++ },
)
2026-04-01 00:53:34 +00:00
p.Parse(makeResultLine())
p.Parse(makeAssistantLine("late"))
2026-03-30 14:09:15 +00:00
if len(texts) != 0 {
t.Fatalf("expected no text after done, got %v", texts)
}
if doneCount != 1 {
t.Fatalf("expected onDone called once, got %d", doneCount)
}
}
func TestStreamParserIgnoresNonAssistantLines(t *testing.T) {
var texts []string
2026-04-01 00:53:34 +00:00
p := CreateStreamParser(
2026-03-30 14:09:15 +00:00
func(text string) { texts = append(texts, text) },
func() {},
)
b1, _ := json.Marshal(map[string]interface{}{"type": "user", "message": map[string]interface{}{}})
2026-04-01 00:53:34 +00:00
p.Parse(string(b1))
2026-03-30 14:09:15 +00:00
b2, _ := json.Marshal(map[string]interface{}{
"type": "assistant",
"message": map[string]interface{}{"content": []interface{}{}},
})
2026-04-01 00:53:34 +00:00
p.Parse(string(b2))
p.Parse(`{"type":"assistant","message":{"content":[{"type":"code","text":"x"}]}}`)
2026-03-30 14:09:15 +00:00
if len(texts) != 0 {
t.Fatalf("expected no texts, got %v", texts)
}
}
func TestStreamParserIgnoresParseErrors(t *testing.T) {
var texts []string
doneCount := 0
2026-04-01 00:53:34 +00:00
p := CreateStreamParser(
2026-03-30 14:09:15 +00:00
func(text string) { texts = append(texts, text) },
func() { doneCount++ },
)
2026-04-01 00:53:34 +00:00
p.Parse("not json")
p.Parse("{")
p.Parse("")
2026-03-30 14:09:15 +00:00
if len(texts) != 0 || doneCount != 0 {
t.Fatalf("expected nothing, got texts=%v done=%d", texts, doneCount)
}
}
func TestStreamParserJoinsMultipleTextParts(t *testing.T) {
var texts []string
2026-04-01 00:53:34 +00:00
p := CreateStreamParser(
2026-03-30 14:09:15 +00:00
func(text string) { texts = append(texts, text) },
func() {},
)
obj := map[string]interface{}{
"type": "assistant",
"message": map[string]interface{}{
"content": []map[string]interface{}{
{"type": "text", "text": "Hello"},
{"type": "text", "text": " "},
{"type": "text", "text": "world"},
},
},
}
b, _ := json.Marshal(obj)
2026-04-01 00:53:34 +00:00
p.Parse(string(b))
2026-03-30 14:09:15 +00:00
if len(texts) != 1 || texts[0] != "Hello world" {
t.Fatalf("expected ['Hello world'], got %v", texts)
}
}
2026-04-01 00:53:34 +00:00
func TestStreamParserFlushTriggersDone(t *testing.T) {
var texts []string
doneCount := 0
p := CreateStreamParser(
func(text string) { texts = append(texts, text) },
func() { doneCount++ },
)
p.Parse(makeAssistantLine("Hello"))
// agent 結束但沒有 result/success手動 flush
p.Flush()
if doneCount != 1 {
t.Fatalf("expected onDone called once after Flush, got %d", doneCount)
}
// 再 flush 不應重複觸發
p.Flush()
if doneCount != 1 {
t.Fatalf("expected onDone called only once, got %d", doneCount)
}
}
func TestStreamParserFlushAfterDoneIsNoop(t *testing.T) {
doneCount := 0
p := CreateStreamParser(
func(text string) {},
func() { doneCount++ },
)
p.Parse(makeResultLine())
p.Flush()
if doneCount != 1 {
t.Fatalf("expected onDone called once, got %d", doneCount)
}
}
func makeThinkingLine(thinking string) string {
obj := map[string]interface{}{
"type": "assistant",
"message": map[string]interface{}{
"content": []map[string]interface{}{
{"type": "thinking", "thinking": thinking},
},
},
}
b, _ := json.Marshal(obj)
return string(b)
}
func makeThinkingAndTextLine(thinking, text string) string {
obj := map[string]interface{}{
"type": "assistant",
"message": map[string]interface{}{
"content": []map[string]interface{}{
{"type": "thinking", "thinking": thinking},
{"type": "text", "text": text},
},
},
}
b, _ := json.Marshal(obj)
return string(b)
}
func TestStreamParserWithThinkingCallsOnThinking(t *testing.T) {
var texts []string
var thinkings []string
p := CreateStreamParserWithThinking(
func(text string) { texts = append(texts, text) },
func(thinking string) { thinkings = append(thinkings, thinking) },
func() {},
)
p.Parse(makeThinkingLine("思考中..."))
p.Parse(makeAssistantLine("回答"))
if len(thinkings) != 1 || thinkings[0] != "思考中..." {
t.Fatalf("expected thinkings=['思考中...'], got %v", thinkings)
}
if len(texts) != 1 || texts[0] != "回答" {
t.Fatalf("expected texts=['回答'], got %v", texts)
}
}
func TestStreamParserWithThinkingNilOnThinkingIgnoresThinking(t *testing.T) {
var texts []string
p := CreateStreamParserWithThinking(
func(text string) { texts = append(texts, text) },
nil,
func() {},
)
p.Parse(makeThinkingLine("忽略的思考"))
p.Parse(makeAssistantLine("文字"))
if len(texts) != 1 || texts[0] != "文字" {
t.Fatalf("expected texts=['文字'], got %v", texts)
}
}
func TestStreamParserWithThinkingDeduplication(t *testing.T) {
var thinkings []string
p := CreateStreamParserWithThinking(
func(text string) {},
func(thinking string) { thinkings = append(thinkings, thinking) },
func() {},
)
p.Parse(makeThinkingLine("A"))
p.Parse(makeThinkingLine("B"))
// 重複的完整思考,應被跳過
p.Parse(makeThinkingLine("AB"))
if len(thinkings) != 2 || thinkings[0] != "A" || thinkings[1] != "B" {
t.Fatalf("expected thinkings=['A','B'], got %v", thinkings)
}
}