File size: 4,246 Bytes
1766992
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
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:   "<<CALL_test>>",
		ThinkingEnabled: true,
	})

	var events []models.AssistantEvent
	events = append(events, parser.Feed("Hello <think")...)
	events = append(events, parser.Feed("ing>draft</thinking> world ")...)
	events = append(events, parser.Feed("<<CALL_test>>\n<invoke name=\"lookup\">{\"q\":\"hel")...)
	events = append(events, parser.Feed("lo\"}</invoke>!")...)
	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)
	}
}