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) } }