package utils import ( "github.com/libaxuan/cursor2api-go/models" "encoding/json" "net/http/httptest" "strings" "testing" "github.com/gin-gonic/gin" ) func TestCursorProtocolParserParsesThinkingAndToolCallsAcrossChunks(t *testing.T) { parser := NewCursorProtocolParser(models.CursorParseConfig{ TriggerSignal: "<>", ThinkingEnabled: true, }) var events []models.AssistantEvent events = append(events, parser.Feed("Hello draft world ")...) events = append(events, parser.Feed("<>\n{\"q\":\"hel")...) events = append(events, parser.Feed("lo\"}!")...) events = append(events, parser.Finish()...) if len(events) != 5 { t.Fatalf("event count = %v, want 5", len(events)) } if events[0].Kind != models.AssistantEventText || events[0].Text != "Hello " { t.Fatalf("event[0] = %#v, want text Hello", events[0]) } if events[1].Kind != models.AssistantEventThinking || events[1].Thinking != "draft" { t.Fatalf("event[1] = %#v, want thinking draft", events[1]) } if events[2].Kind != models.AssistantEventText || events[2].Text != " world " { t.Fatalf("event[2] = %#v, want text world", events[2]) } if events[3].Kind != models.AssistantEventToolCall || events[3].ToolCall == nil { t.Fatalf("event[3] = %#v, want tool call", events[3]) } if events[3].ToolCall.Function.Name != "lookup" { t.Fatalf("tool name = %v, want lookup", events[3].ToolCall.Function.Name) } if events[3].ToolCall.Function.Arguments != `{"q":"hello"}` { t.Fatalf("tool arguments = %v, want compact json", events[3].ToolCall.Function.Arguments) } if events[4].Kind != models.AssistantEventText || events[4].Text != "!" { t.Fatalf("event[4] = %#v, want trailing exclamation text", events[4]) } } func TestNonStreamChatCompletionReturnsToolCalls(t *testing.T) { gin.SetMode(gin.TestMode) recorder := httptest.NewRecorder() ctx, _ := gin.CreateTestContext(recorder) ctx.Request = httptest.NewRequest("POST", "/v1/chat/completions", nil) ch := make(chan interface{}, 4) ch <- models.AssistantEvent{Kind: models.AssistantEventText, Text: "Let me check."} ch <- models.AssistantEvent{ Kind: models.AssistantEventToolCall, ToolCall: &models.ToolCall{ ID: "call_1", Type: "function", Function: models.FunctionCall{ Name: "lookup", Arguments: `{"q":"revivalquant"}`, }, }, } ch <- models.Usage{PromptTokens: 10, CompletionTokens: 5, TotalTokens: 15} close(ch) NonStreamChatCompletion(ctx, ch, "claude-sonnet-4.6") var response models.ChatCompletionResponse if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil { t.Fatalf("unmarshal response: %v", err) } if response.Choices[0].FinishReason != "tool_calls" { t.Fatalf("finish reason = %v, want tool_calls", response.Choices[0].FinishReason) } if response.Choices[0].Message.ToolCalls[0].Function.Name != "lookup" { t.Fatalf("tool call name = %v, want lookup", response.Choices[0].Message.ToolCalls[0].Function.Name) } if response.Choices[0].Message.Content != "Let me check." { t.Fatalf("message content = %#v, want Let me check.", response.Choices[0].Message.Content) } } func TestStreamChatCompletionEmitsToolCallChunks(t *testing.T) { gin.SetMode(gin.TestMode) recorder := httptest.NewRecorder() ctx, _ := gin.CreateTestContext(recorder) ctx.Request = httptest.NewRequest("POST", "/v1/chat/completions", nil) ch := make(chan interface{}, 2) ch <- models.AssistantEvent{ Kind: models.AssistantEventToolCall, ToolCall: &models.ToolCall{ ID: "call_1", Type: "function", Function: models.FunctionCall{ Name: "lookup", Arguments: `{"q":"revivalquant"}`, }, }, } close(ch) StreamChatCompletion(ctx, ch, "claude-sonnet-4.6") body := recorder.Body.String() if !strings.Contains(body, `"tool_calls":[{"index":0,"id":"call_1","type":"function"`) { t.Fatalf("stream body missing tool_calls delta: %s", body) } if !strings.Contains(body, `"finish_reason":"tool_calls"`) { t.Fatalf("stream body missing tool_calls finish reason: %s", body) } if !strings.Contains(body, "[DONE]") { t.Fatalf("stream body missing DONE marker: %s", body) } }