206 lines
5.1 KiB
Go
206 lines
5.1 KiB
Go
package service
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// TestConvertClaudeToolsToGeminiTools_CustomType 测试custom类型工具转换
|
|
func TestConvertClaudeToolsToGeminiTools_CustomType(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
tools any
|
|
expectedLen int
|
|
description string
|
|
}{
|
|
{
|
|
name: "Standard tools",
|
|
tools: []any{
|
|
map[string]any{
|
|
"name": "get_weather",
|
|
"description": "Get weather info",
|
|
"input_schema": map[string]any{"type": "object"},
|
|
},
|
|
},
|
|
expectedLen: 1,
|
|
description: "标准工具格式应该正常转换",
|
|
},
|
|
{
|
|
name: "Custom type tool (MCP format)",
|
|
tools: []any{
|
|
map[string]any{
|
|
"type": "custom",
|
|
"name": "mcp_tool",
|
|
"custom": map[string]any{
|
|
"description": "MCP tool description",
|
|
"input_schema": map[string]any{"type": "object"},
|
|
},
|
|
},
|
|
},
|
|
expectedLen: 1,
|
|
description: "Custom类型工具应该从custom字段读取",
|
|
},
|
|
{
|
|
name: "Mixed standard and custom tools",
|
|
tools: []any{
|
|
map[string]any{
|
|
"name": "standard_tool",
|
|
"description": "Standard",
|
|
"input_schema": map[string]any{"type": "object"},
|
|
},
|
|
map[string]any{
|
|
"type": "custom",
|
|
"name": "custom_tool",
|
|
"custom": map[string]any{
|
|
"description": "Custom",
|
|
"input_schema": map[string]any{"type": "object"},
|
|
},
|
|
},
|
|
},
|
|
expectedLen: 1,
|
|
description: "混合工具应该都能正确转换",
|
|
},
|
|
{
|
|
name: "Custom tool without custom field",
|
|
tools: []any{
|
|
map[string]any{
|
|
"type": "custom",
|
|
"name": "invalid_custom",
|
|
// 缺少 custom 字段
|
|
},
|
|
},
|
|
expectedLen: 0, // 应该被跳过
|
|
description: "缺少custom字段的custom工具应该被跳过",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := convertClaudeToolsToGeminiTools(tt.tools)
|
|
|
|
if tt.expectedLen == 0 {
|
|
if result != nil {
|
|
t.Errorf("%s: expected nil result, got %v", tt.description, result)
|
|
}
|
|
return
|
|
}
|
|
|
|
if result == nil {
|
|
t.Fatalf("%s: expected non-nil result", tt.description)
|
|
}
|
|
|
|
if len(result) != 1 {
|
|
t.Errorf("%s: expected 1 tool declaration, got %d", tt.description, len(result))
|
|
return
|
|
}
|
|
|
|
toolDecl, ok := result[0].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("%s: result[0] is not map[string]any", tt.description)
|
|
}
|
|
|
|
funcDecls, ok := toolDecl["functionDeclarations"].([]any)
|
|
if !ok {
|
|
t.Fatalf("%s: functionDeclarations is not []any", tt.description)
|
|
}
|
|
|
|
toolsArr, _ := tt.tools.([]any)
|
|
expectedFuncCount := 0
|
|
for _, tool := range toolsArr {
|
|
toolMap, _ := tool.(map[string]any)
|
|
if toolMap["name"] != "" {
|
|
// 检查是否为有效的custom工具
|
|
if toolMap["type"] == "custom" {
|
|
if toolMap["custom"] != nil {
|
|
expectedFuncCount++
|
|
}
|
|
} else {
|
|
expectedFuncCount++
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(funcDecls) != expectedFuncCount {
|
|
t.Errorf("%s: expected %d function declarations, got %d",
|
|
tt.description, expectedFuncCount, len(funcDecls))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestConvertClaudeMessagesToGeminiGenerateContent_AddsThoughtSignatureForToolUse(t *testing.T) {
|
|
claudeReq := map[string]any{
|
|
"model": "claude-haiku-4-5-20251001",
|
|
"max_tokens": 10,
|
|
"messages": []any{
|
|
map[string]any{
|
|
"role": "user",
|
|
"content": []any{
|
|
map[string]any{"type": "text", "text": "hi"},
|
|
},
|
|
},
|
|
map[string]any{
|
|
"role": "assistant",
|
|
"content": []any{
|
|
map[string]any{"type": "text", "text": "ok"},
|
|
map[string]any{
|
|
"type": "tool_use",
|
|
"id": "toolu_123",
|
|
"name": "default_api:write_file",
|
|
"input": map[string]any{"path": "a.txt", "content": "x"},
|
|
// no signature on purpose
|
|
},
|
|
},
|
|
},
|
|
},
|
|
"tools": []any{
|
|
map[string]any{
|
|
"name": "default_api:write_file",
|
|
"description": "write file",
|
|
"input_schema": map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{"path": map[string]any{"type": "string"}},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
b, _ := json.Marshal(claudeReq)
|
|
|
|
out, err := convertClaudeMessagesToGeminiGenerateContent(b)
|
|
if err != nil {
|
|
t.Fatalf("convert failed: %v", err)
|
|
}
|
|
s := string(out)
|
|
if !strings.Contains(s, "\"functionCall\"") {
|
|
t.Fatalf("expected functionCall in output, got: %s", s)
|
|
}
|
|
if !strings.Contains(s, "\"thoughtSignature\":\""+geminiDummyThoughtSignature+"\"") {
|
|
t.Fatalf("expected injected thoughtSignature %q, got: %s", geminiDummyThoughtSignature, s)
|
|
}
|
|
}
|
|
|
|
func TestEnsureGeminiFunctionCallThoughtSignatures_InsertsWhenMissing(t *testing.T) {
|
|
geminiReq := map[string]any{
|
|
"contents": []any{
|
|
map[string]any{
|
|
"role": "user",
|
|
"parts": []any{
|
|
map[string]any{
|
|
"functionCall": map[string]any{
|
|
"name": "default_api:write_file",
|
|
"args": map[string]any{"path": "a.txt"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
b, _ := json.Marshal(geminiReq)
|
|
out := ensureGeminiFunctionCallThoughtSignatures(b)
|
|
s := string(out)
|
|
if !strings.Contains(s, "\"thoughtSignature\":\""+geminiDummyThoughtSignature+"\"") {
|
|
t.Fatalf("expected injected thoughtSignature %q, got: %s", geminiDummyThoughtSignature, s)
|
|
}
|
|
}
|