package main import ( "encoding/json" "testing" "github.com/sashabaranov/go-openai" ) // TestLLM_ConvertMCPToolsToOpenAI tests the MCP to OpenAI tool conversion func TestLLM_ConvertMCPToolsToOpenAI(t *testing.T) { tests := []struct { name string mcpTools []Tool wantLen int }{ { name: "EmptyTools", mcpTools: []Tool{}, wantLen: 0, }, { name: "SingleTool", mcpTools: []Tool{ { Name: "introspect", Description: "Discover the GraphQL schema", InputSchema: InputSchema{ Type: "object", Properties: map[string]Property{ "typeName": {Type: "string", Description: "The type to introspect"}, }, Required: []string{}, AdditionalProperties: false, }, }, }, wantLen: 1, }, { name: "MultipleTools", mcpTools: []Tool{ { Name: "query", Description: "Execute a GraphQL query", InputSchema: InputSchema{ Type: "object", Properties: map[string]Property{ "query": {Type: "string", Description: "The GraphQL query"}, }, Required: []string{"query"}, AdditionalProperties: false, }, }, { Name: "mutate", Description: "Execute a GraphQL mutation", InputSchema: InputSchema{ Type: "object", Properties: map[string]Property{ "mutation": {Type: "string", Description: "The GraphQL mutation"}, }, Required: []string{"mutation"}, AdditionalProperties: false, }, }, }, wantLen: 2, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { tools := ConvertMCPToolsToOpenAI(tt.mcpTools) if len(tools) != tt.wantLen { t.Errorf("Expected %d tools, got %d", tt.wantLen, len(tools)) } // Verify tool conversion details for i, tool := range tools { if tool.Type != openai.ToolTypeFunction { t.Errorf("Tool %d: Expected type %s, got %s", i, openai.ToolTypeFunction, tool.Type) } if tool.Function.Name != tt.mcpTools[i].Name { t.Errorf("Tool %d: Expected name %s, got %s", i, tt.mcpTools[i].Name, tool.Function.Name) } if tool.Function.Description != tt.mcpTools[i].Description { t.Errorf("Tool %d: Expected description %s, got %s", i, tt.mcpTools[i].Description, tool.Function.Description) } } }) } } // TestLLM_ConvertMCPToolsToOpenAI_ObjectProperties tests that object-type properties // get additionalProperties: true to allow arbitrary key-value pairs func TestLLM_ConvertMCPToolsToOpenAI_ObjectProperties(t *testing.T) { mcpTools := []Tool{ { Name: "query", Description: "Execute a GraphQL query", InputSchema: InputSchema{ Type: "object", Properties: map[string]Property{ "query": { Type: "string", Description: "The GraphQL query string", }, "variables": { Type: "object", Description: "Optional query variables as key-value pairs", }, }, Required: []string{"query"}, AdditionalProperties: false, }, }, } tools := ConvertMCPToolsToOpenAI(mcpTools) if len(tools) != 1 { t.Fatalf("Expected 1 tool, got %d", len(tools)) } // Check that parameters don't have additionalProperties at top level params := tools[0].Function.Parameters.(map[string]interface{}) if _, hasAdditionalProps := params["additionalProperties"]; hasAdditionalProps { t.Error("Top-level parameters should NOT have additionalProperties field") } // Check that the variables property has additionalProperties: true props := params["properties"].(map[string]interface{}) variablesProp, ok := props["variables"].(map[string]interface{}) if !ok { t.Fatal("variables property not found") } if additionalProps, ok := variablesProp["additionalProperties"]; !ok { t.Error("Object property 'variables' should have additionalProperties field") } else if additionalProps != true { t.Errorf("Object property 'variables' additionalProperties should be true, got %v", additionalProps) } // Check that string property does NOT have additionalProperties queryProp, ok := props["query"].(map[string]interface{}) if !ok { t.Fatal("query property not found") } if _, hasAdditionalProps := queryProp["additionalProperties"]; hasAdditionalProps { t.Error("String property 'query' should NOT have additionalProperties field") } } // TestLLM_ParseToolCall tests parsing tool calls from LLM responses func TestLLM_ParseToolCall(t *testing.T) { tests := []struct { name string toolCall openai.ToolCall wantName string wantArgs map[string]interface{} wantErr bool }{ { name: "ValidToolCall", toolCall: openai.ToolCall{ ID: "call-123", Function: openai.FunctionCall{ Name: "query", Arguments: `{"query": "{ users { email } }"}`, }, }, wantName: "query", wantArgs: map[string]interface{}{ "query": "{ users { email } }", }, wantErr: false, }, { name: "EmptyArguments", toolCall: openai.ToolCall{ ID: "call-456", Function: openai.FunctionCall{ Name: "introspect", Arguments: `{}`, }, }, wantName: "introspect", wantArgs: map[string]interface{}{}, wantErr: false, }, { name: "InvalidJSON", toolCall: openai.ToolCall{ ID: "call-789", Function: openai.FunctionCall{ Name: "mutate", Arguments: `invalid json`, }, }, wantName: "mutate", wantArgs: nil, wantErr: true, }, { name: "NestedArguments", toolCall: openai.ToolCall{ ID: "call-abc", Function: openai.FunctionCall{ Name: "createTask", Arguments: `{"title": "Test Task", "priority": "high", "assigneeId": "user-123"}`, }, }, wantName: "createTask", wantArgs: map[string]interface{}{ "title": "Test Task", "priority": "high", "assigneeId": "user-123", }, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { name, args, err := ParseToolCall(tt.toolCall) if (err != nil) != tt.wantErr { t.Errorf("ParseToolCall() error = %v, wantErr %v", err, tt.wantErr) return } if name != tt.wantName { t.Errorf("ParseToolCall() name = %v, want %v", name, tt.wantName) } if !tt.wantErr && args != nil { // Compare args argsJSON, _ := json.Marshal(args) wantJSON, _ := json.Marshal(tt.wantArgs) if string(argsJSON) != string(wantJSON) { t.Errorf("ParseToolCall() args = %v, want %v", args, tt.wantArgs) } } }) } } // TestLLM_ToolConversionSnapshot tests tool conversion with snapshot func TestLLM_ToolConversionSnapshot(t *testing.T) { mcpTools := []Tool{ { Name: "introspect", Description: "Discover the GraphQL schema structure", InputSchema: InputSchema{ Type: "object", Properties: map[string]Property{ "typeName": { Type: "string", Description: "Optional type name to introspect", }, }, Required: []string{}, AdditionalProperties: false, }, }, { Name: "query", Description: "Execute a GraphQL query", InputSchema: InputSchema{ Type: "object", Properties: map[string]Property{ "query": { Type: "string", Description: "The GraphQL query string", }, }, Required: []string{"query"}, AdditionalProperties: false, }, }, { Name: "mutate", Description: "Execute a GraphQL mutation", InputSchema: InputSchema{ Type: "object", Properties: map[string]Property{ "mutation": { Type: "string", Description: "The GraphQL mutation string", }, }, Required: []string{"mutation"}, AdditionalProperties: false, }, }, } openaiTools := ConvertMCPToolsToOpenAI(mcpTools) testSnapshotResult(t, "converted_tools", openaiTools) }