281 lines
6.9 KiB
Go
281 lines
6.9 KiB
Go
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)
|
||
}
|
||
|
||
func TestStreamParserFragmentMode(t *testing.T) {
|
||
// cursor --stream-partial-output 模式:每個訊息是獨立 token fragment
|
||
var texts []string
|
||
p := CreateStreamParser(
|
||
func(text string) { texts = append(texts, text) },
|
||
func() {},
|
||
)
|
||
|
||
p.Parse(makeAssistantLine("你"))
|
||
p.Parse(makeAssistantLine("好!有"))
|
||
p.Parse(makeAssistantLine("什"))
|
||
p.Parse(makeAssistantLine("麼"))
|
||
|
||
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)
|
||
}
|
||
}
|
||
|
||
func TestStreamParserDeduplicatesFinalFullText(t *testing.T) {
|
||
// 最後一個訊息是完整的累積文字,應被跳過(去重)
|
||
var texts []string
|
||
p := CreateStreamParser(
|
||
func(text string) { texts = append(texts, text) },
|
||
func() {},
|
||
)
|
||
|
||
p.Parse(makeAssistantLine("Hello"))
|
||
p.Parse(makeAssistantLine(" world"))
|
||
// 最後一個是完整累積文字,應被去重
|
||
p.Parse(makeAssistantLine("Hello world"))
|
||
|
||
if len(texts) != 2 {
|
||
t.Fatalf("expected 2 fragments (final full text deduplicated), got %d: %v", len(texts), texts)
|
||
}
|
||
if texts[0] != "Hello" || texts[1] != " world" {
|
||
t.Fatalf("unexpected texts: %v", texts)
|
||
}
|
||
}
|
||
|
||
func TestStreamParserCallsOnDone(t *testing.T) {
|
||
var texts []string
|
||
doneCount := 0
|
||
p := CreateStreamParser(
|
||
func(text string) { texts = append(texts, text) },
|
||
func() { doneCount++ },
|
||
)
|
||
|
||
p.Parse(makeResultLine())
|
||
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
|
||
p := CreateStreamParser(
|
||
func(text string) { texts = append(texts, text) },
|
||
func() { doneCount++ },
|
||
)
|
||
|
||
p.Parse(makeResultLine())
|
||
p.Parse(makeAssistantLine("late"))
|
||
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
|
||
p := CreateStreamParser(
|
||
func(text string) { texts = append(texts, text) },
|
||
func() {},
|
||
)
|
||
|
||
b1, _ := json.Marshal(map[string]interface{}{"type": "user", "message": map[string]interface{}{}})
|
||
p.Parse(string(b1))
|
||
b2, _ := json.Marshal(map[string]interface{}{
|
||
"type": "assistant",
|
||
"message": map[string]interface{}{"content": []interface{}{}},
|
||
})
|
||
p.Parse(string(b2))
|
||
p.Parse(`{"type":"assistant","message":{"content":[{"type":"code","text":"x"}]}}`)
|
||
|
||
if len(texts) != 0 {
|
||
t.Fatalf("expected no texts, got %v", texts)
|
||
}
|
||
}
|
||
|
||
func TestStreamParserIgnoresParseErrors(t *testing.T) {
|
||
var texts []string
|
||
doneCount := 0
|
||
p := CreateStreamParser(
|
||
func(text string) { texts = append(texts, text) },
|
||
func() { doneCount++ },
|
||
)
|
||
|
||
p.Parse("not json")
|
||
p.Parse("{")
|
||
p.Parse("")
|
||
|
||
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
|
||
p := CreateStreamParser(
|
||
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)
|
||
p.Parse(string(b))
|
||
|
||
if len(texts) != 1 || texts[0] != "Hello world" {
|
||
t.Fatalf("expected ['Hello world'], got %v", texts)
|
||
}
|
||
}
|
||
|
||
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)
|
||
}
|
||
}
|