Files
hygate/hygate_test.go
2026-07-31 10:37:20 +03:00

320 lines
10 KiB
Go

package main
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func TestHunyuanServiceEndpoint(t *testing.T) {
svc := NewHunyuanService("https://example.com/gradio/", "")
if svc.endpoint != "https://example.com/gradio" {
t.Errorf("expected trailing slash stripped, got %q", svc.endpoint)
}
models := svc.ListModels()
if len(models) != 1 || models[0].ID != "hy3" {
t.Fatalf("expected model ID 'hy3', got %v", models)
}
}
func TestCustomModelNameConfiguration(t *testing.T) {
svc := NewHunyuanService("https://example.com/gradio", "hunyuan-custom-v1")
if svc.modelName != "hunyuan-custom-v1" {
t.Errorf("expected modelName 'hunyuan-custom-v1', got %q", svc.modelName)
}
models := svc.ListModels()
if len(models) != 1 || models[0].ID != "hunyuan-custom-v1" {
t.Fatalf("expected model ID 'hunyuan-custom-v1', got %v", models)
}
}
func TestParseGradioStreamData(t *testing.T) {
sampleJSON := `[["Hello world", "Thinking process...", null]]`
c, r, tc, ok := parseGradioStreamData(sampleJSON)
if !ok {
t.Fatalf("expected parseGradioStreamData to succeed")
}
if c != "Hello world" {
t.Errorf("content=%q, want 'Hello world'", c)
}
if r != "Thinking process..." {
t.Errorf("reasoning=%q, want 'Thinking process...'", r)
}
if len(tc) != 0 {
t.Errorf("expected 0 tool calls, got %d", len(tc))
}
}
func TestHunyuanMockedCompletion(t *testing.T) {
var receivedCallURL, receivedStreamURL string
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.HasSuffix(r.URL.Path, "/gradio_api/call/chat") && r.Method == http.MethodPost {
receivedCallURL = r.URL.Path
w.Header().Set("Content-Type", "application/json")
fmt.Fprint(w, `{"event_id":"test-event-123"}`)
return
}
if strings.Contains(r.URL.Path, "/gradio_api/call/chat/test-event-123") && r.Method == http.MethodGet {
receivedStreamURL = r.URL.Path
w.Header().Set("Content-Type", "text/event-stream")
fmt.Fprint(w, "data: [[\"Mocked answer\", \"Mocked reasoning\", null]]\n\n")
return
}
http.NotFound(w, r)
})
server := httptest.NewServer(handler)
defer server.Close()
svc := NewHunyuanService(server.URL, "hy3")
rec := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/v1/chat/completions", nil)
chatReq := ChatCompletionRequest{
Model: "hy3",
Messages: []ChatMessage{
{Role: "user", Content: "Hello!"},
},
Stream: false,
}
err := svc.Chat(rec, req, chatReq)
if err != nil {
t.Fatalf("unexpected Chat error: %v", err)
}
if receivedCallURL != "/gradio_api/call/chat" {
t.Errorf("expected call URL '/gradio_api/call/chat', got %q", receivedCallURL)
}
if receivedStreamURL != "/gradio_api/call/chat/test-event-123" {
t.Errorf("expected stream URL '/gradio_api/call/chat/test-event-123', got %q", receivedStreamURL)
}
var resp ChatCompletionResponse
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("failed to decode completion response: %v", err)
}
if len(resp.Choices) != 1 {
t.Fatalf("expected 1 choice, got %d", len(resp.Choices))
}
if resp.Choices[0].Message.GetContentString() != "Mocked answer" {
t.Errorf("content=%q, want 'Mocked answer'", resp.Choices[0].Message.GetContentString())
}
if resp.Choices[0].Message.ReasoningContent != "Mocked reasoning" {
t.Errorf("reasoning=%q, want 'Mocked reasoning'", resp.Choices[0].Message.ReasoningContent)
}
}
func TestParseGradioStreamDataWithToolCalls(t *testing.T) {
sampleJSON := `[["", "Let me check the weather.", [{"id":"call_999","type":"function","function":{"name":"get_weather","arguments":"{\"location\":\"Tokyo\"}"}}]]]`
c, r, tc, ok := parseGradioStreamData(sampleJSON)
if !ok {
t.Fatalf("expected parseGradioStreamData to succeed")
}
if c != "" {
t.Errorf("content=%q, want empty", c)
}
if r != "Let me check the weather." {
t.Errorf("reasoning=%q, want 'Let me check the weather.'", r)
}
if len(tc) != 1 {
t.Fatalf("expected 1 tool call, got %d", len(tc))
}
if tc[0].ID != "call_999" || tc[0].Function.Name != "get_weather" || tc[0].Function.Arguments != `{"location":"Tokyo"}` {
t.Errorf("unexpected tool call payload: %+v", tc[0])
}
}
func TestHunyuanToolCallingNonStreaming(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.HasSuffix(r.URL.Path, "/gradio_api/call/chat") && r.Method == http.MethodPost {
var body map[string]interface{}
json.NewDecoder(r.Body).Decode(&body)
dataArr, _ := body["data"].([]interface{})
if len(dataArr) < 9 || dataArr[8] == nil || dataArr[8] == "" {
t.Errorf("expected functionsJSONStr in data[8], got %v", dataArr)
}
w.Header().Set("Content-Type", "application/json")
fmt.Fprint(w, `{"event_id":"evt-tool-1"}`)
return
}
if strings.Contains(r.URL.Path, "/gradio_api/call/chat/evt-tool-1") && r.Method == http.MethodGet {
w.Header().Set("Content-Type", "text/event-stream")
fmt.Fprint(w, "data: [[\"\", \"I should call the search tool.\", [{\"id\":\"call_abc\",\"type\":\"function\",\"function\":{\"name\":\"search\",\"arguments\":\"{\\\"query\\\":\\\"golang\\\"}\"}}]]]\n\n")
return
}
http.NotFound(w, r)
})
server := httptest.NewServer(handler)
defer server.Close()
svc := NewHunyuanService(server.URL, "hy3")
rec := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/v1/chat/completions", nil)
chatReq := ChatCompletionRequest{
Model: "hy3",
Messages: []ChatMessage{
{Role: "user", Content: "Search for golang news"},
},
Tools: []Tool{
{
Type: "function",
Function: map[string]interface{}{
"name": "search",
"description": "Search web",
},
},
},
Stream: false,
}
err := svc.Chat(rec, req, chatReq)
if err != nil {
t.Fatalf("unexpected Chat error: %v", err)
}
var resp ChatCompletionResponse
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("failed to decode response: %v", err)
}
if len(resp.Choices) != 1 {
t.Fatalf("expected 1 choice, got %d", len(resp.Choices))
}
choice := resp.Choices[0]
if choice.FinishReason != "tool_calls" {
t.Errorf("finish_reason=%q, want 'tool_calls'", choice.FinishReason)
}
if choice.Message.ReasoningContent != "I should call the search tool." {
t.Errorf("reasoning_content=%q, want 'I should call the search tool.'", choice.Message.ReasoningContent)
}
if len(choice.Message.ToolCalls) != 1 {
t.Fatalf("expected 1 tool call, got %d", len(choice.Message.ToolCalls))
}
tc := choice.Message.ToolCalls[0]
if tc.ID != "call_abc" || tc.Function.Name != "search" || tc.Function.Arguments != `{"query":"golang"}` {
t.Errorf("unexpected tool call: %+v", tc)
}
}
func TestHunyuanReasoningAndToolCallingStreaming(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.HasSuffix(r.URL.Path, "/gradio_api/call/chat") && r.Method == http.MethodPost {
w.Header().Set("Content-Type", "application/json")
fmt.Fprint(w, `{"event_id":"evt-stream-1"}`)
return
}
if strings.Contains(r.URL.Path, "/gradio_api/call/chat/evt-stream-1") && r.Method == http.MethodGet {
w.Header().Set("Content-Type", "text/event-stream")
flusher, _ := w.(http.Flusher)
fmt.Fprint(w, "data: [[\"\", \"Step 1 reasoning.\", null]]\n\n")
if flusher != nil {
flusher.Flush()
}
fmt.Fprint(w, "data: [[\"Hello \", \"Step 1 reasoning. Step 2 reasoning.\", null]]\n\n")
if flusher != nil {
flusher.Flush()
}
fmt.Fprint(w, "data: [[\"Hello \", \"Step 1 reasoning. Step 2 reasoning.\", [{\"id\":\"call_xyz\",\"type\":\"function\",\"function\":{\"name\":\"calculator\",\"arguments\":\"{\\\"expr\\\":\\\"2+2\\\"}\"}}]]]\n\n")
if flusher != nil {
flusher.Flush()
}
return
}
http.NotFound(w, r)
})
server := httptest.NewServer(handler)
defer server.Close()
svc := NewHunyuanService(server.URL, "hy3")
rec := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/v1/chat/completions", nil)
chatReq := ChatCompletionRequest{
Model: "hy3",
Messages: []ChatMessage{
{Role: "user", Content: "Calculate 2+2"},
},
Stream: true,
}
err := svc.Chat(rec, req, chatReq)
if err != nil {
t.Fatalf("unexpected Chat error: %v", err)
}
bodyStr := rec.Body.String()
lines := strings.Split(bodyStr, "\n")
var receivedDeltas []StreamDelta
var finishReasons []string
var foundDone bool
for _, line := range lines {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "data: ") {
payload := strings.TrimPrefix(line, "data: ")
if payload == "[DONE]" {
foundDone = true
continue
}
var streamResp StreamResponse
if err := json.Unmarshal([]byte(payload), &streamResp); err == nil && len(streamResp.Choices) > 0 {
choice := streamResp.Choices[0]
receivedDeltas = append(receivedDeltas, choice.Delta)
if choice.FinishReason != nil {
finishReasons = append(finishReasons, *choice.FinishReason)
}
}
}
}
if !foundDone {
t.Errorf("expected 'data: [DONE]' in SSE stream")
}
if len(receivedDeltas) < 1 || receivedDeltas[0].Role != "assistant" {
t.Errorf("expected first delta to set role='assistant', got %+v", receivedDeltas[0])
}
var combinedReasoning, combinedContent string
var toolCallsEmitted []ToolCall
for _, d := range receivedDeltas {
combinedReasoning += d.ReasoningContent
combinedContent += d.Content
if len(d.ToolCalls) > 0 {
toolCallsEmitted = append(toolCallsEmitted, d.ToolCalls...)
}
}
if combinedReasoning != "Step 1 reasoning. Step 2 reasoning." {
t.Errorf("combined reasoning=%q, want 'Step 1 reasoning. Step 2 reasoning.'", combinedReasoning)
}
if combinedContent != "Hello " {
t.Errorf("combined content=%q, want 'Hello '", combinedContent)
}
if len(toolCallsEmitted) == 0 {
t.Fatalf("expected tool call deltas to be emitted")
}
if len(finishReasons) != 1 || finishReasons[0] != "tool_calls" {
t.Errorf("finish_reasons=%v, want ['tool_calls']", finishReasons)
}
}