package services import ( "github.com/libaxuan/cursor2api-go/config" "github.com/libaxuan/cursor2api-go/models" "strings" "testing" ) func TestBuildCursorRequestEnablesToolProtocolForBaseModel(t *testing.T) { service := &CursorService{ config: &config.Config{ SystemPromptInject: "Injected system prompt", MaxInputLength: 10000, }, } request := &models.ChatCompletionRequest{ Model: "claude-sonnet-4.6", Messages: []models.Message{ {Role: "user", Content: "What's the weather?"}, }, Tools: []models.Tool{ { Type: "function", Function: models.FunctionDefinition{ Name: "get_weather", Description: "Fetch current weather", Parameters: map[string]interface{}{ "type": "object", }, }, }, }, } result, err := service.buildCursorRequest(request) if err != nil { t.Fatalf("buildCursorRequest() error = %v", err) } if result.Payload.Model != "anthropic/claude-sonnet-4.6" { t.Fatalf("Payload.Model = %v, want anthropic/claude-sonnet-4.6", result.Payload.Model) } if result.ParseConfig.TriggerSignal == "" { t.Fatalf("TriggerSignal should not be empty") } if result.ParseConfig.ThinkingEnabled { t.Fatalf("ThinkingEnabled = true, want false") } systemText := result.Payload.Messages[0].Parts[0].Text if !strings.Contains(systemText, "") { t.Fatalf("system prompt does not include function list: %s", systemText) } if strings.Contains(systemText, thinkingHint) { t.Fatalf("system prompt should not include thinking hint for base model") } } func TestBuildCursorRequestThinkingModelFormatsToolHistory(t *testing.T) { service := &CursorService{ config: &config.Config{ MaxInputLength: 10000, }, } request := &models.ChatCompletionRequest{ Model: "claude-sonnet-4.6-thinking", Messages: []models.Message{ {Role: "user", Content: "Plan first, then use tools."}, { Role: "assistant", ToolCalls: []models.ToolCall{ { ID: "call_1", Type: "function", Function: models.FunctionCall{ Name: "lookup", Arguments: `{"q":"revivalquant"}`, }, }, }, }, {Role: "tool", ToolCallID: "call_1", Name: "lookup", Content: "Found result"}, }, Tools: []models.Tool{ { Type: "function", Function: models.FunctionDefinition{ Name: "lookup", }, }, }, } result, err := service.buildCursorRequest(request) if err != nil { t.Fatalf("buildCursorRequest() error = %v", err) } if result.ParseConfig.TriggerSignal == "" { t.Fatalf("TriggerSignal should not be empty") } if !result.ParseConfig.ThinkingEnabled { t.Fatalf("ThinkingEnabled = false, want true") } if result.Payload.Model != "anthropic/claude-sonnet-4.6" { t.Fatalf("Payload.Model = %v, want anthropic/claude-sonnet-4.6", result.Payload.Model) } userText := result.Payload.Messages[1].Parts[0].Text if !strings.Contains(userText, thinkingHint) { t.Fatalf("user message should contain thinking hint, got: %s", userText) } assistantText := result.Payload.Messages[2].Parts[0].Text if !strings.Contains(assistantText, result.ParseConfig.TriggerSignal) { t.Fatalf("assistant tool history should include trigger signal, got: %s", assistantText) } if !strings.Contains(assistantText, `{"q":"revivalquant"}`) { t.Fatalf("assistant tool history missing invoke block, got: %s", assistantText) } toolText := result.Payload.Messages[3].Parts[0].Text if !strings.Contains(toolText, `Found result`) { t.Fatalf("tool result history missing tool_result block, got: %s", toolText) } } func TestBuildCursorRequestPreservesToolHistoryWithoutCurrentTools(t *testing.T) { service := &CursorService{ config: &config.Config{ MaxInputLength: 10000, }, } request := &models.ChatCompletionRequest{ Model: "claude-sonnet-4.6", ToolChoice: []byte(`"none"`), Messages: []models.Message{ { Role: "assistant", ToolCalls: []models.ToolCall{ { ID: "call_weather", Type: "function", Function: models.FunctionCall{ Name: "get_weather", Arguments: `{"city":"Beijing"}`, }, }, }, }, {Role: "tool", ToolCallID: "call_weather", Name: "get_weather", Content: "Sunny"}, {Role: "user", Content: "Summarize the result."}, }, } result, err := service.buildCursorRequest(request) if err != nil { t.Fatalf("buildCursorRequest() error = %v", err) } if result.ParseConfig.TriggerSignal == "" { t.Fatalf("TriggerSignal should be kept for tool history replay") } systemText := result.Payload.Messages[0].Parts[0].Text if !strings.Contains(systemText, "completed history") { t.Fatalf("system prompt should explain historical tool transcript, got: %s", systemText) } if !strings.Contains(systemText, result.ParseConfig.TriggerSignal) { t.Fatalf("system prompt should include trigger signal, got: %s", systemText) } assistantText := result.Payload.Messages[1].Parts[0].Text if !strings.Contains(assistantText, result.ParseConfig.TriggerSignal) { t.Fatalf("assistant history should preserve trigger signal, got: %s", assistantText) } } func TestBuildCursorRequestAllowsToolChoiceNoneWithoutTools(t *testing.T) { service := &CursorService{ config: &config.Config{ MaxInputLength: 10000, }, } request := &models.ChatCompletionRequest{ Model: "claude-sonnet-4.6", ToolChoice: []byte(`"none"`), Messages: []models.Message{ {Role: "user", Content: "Hello"}, }, } result, err := service.buildCursorRequest(request) if err != nil { t.Fatalf("buildCursorRequest() error = %v", err) } if result.ParseConfig.TriggerSignal != "" { t.Fatalf("TriggerSignal = %q, want empty for plain chat", result.ParseConfig.TriggerSignal) } if len(result.Payload.Messages) != 1 { t.Fatalf("payload message count = %d, want 1", len(result.Payload.Messages)) } } func TestBuildCursorRequestCountsSerializedToolCallsInMaxInputLength(t *testing.T) { service := &CursorService{ config: &config.Config{ MaxInputLength: 20, }, } request := &models.ChatCompletionRequest{ Model: "claude-sonnet-4.6", Messages: []models.Message{ { Role: "assistant", ToolCalls: []models.ToolCall{ { ID: "call_1", Type: "function", Function: models.FunctionCall{ Name: "lookup", Arguments: `{"payload":"1234567890123456789012345678901234567890"}`, }, }, }, }, {Role: "user", Content: "Short"}, }, } result, err := service.buildCursorRequest(request) if err != nil { t.Fatalf("buildCursorRequest() error = %v", err) } for _, msg := range result.Payload.Messages { if strings.Contains(msg.Parts[0].Text, `"payload":"1234567890123456789012345678901234567890"`) { t.Fatalf("serialized tool call arguments should be removed by truncation, payload still contains long tool json: %#v", result.Payload.Messages) } } totalLength := 0 for _, msg := range result.Payload.Messages { totalLength += len(msg.Parts[0].Text) } if totalLength == 0 { t.Fatalf("truncation should preserve at least one message") } }