first commit

This commit is contained in:
Beyhan Oğur
2026-04-26 21:52:23 +03:00
commit 880f412e2c
2662 changed files with 866266 additions and 0 deletions

View File

@@ -0,0 +1,563 @@
package mcptests
import (
"fmt"
"strings"
"testing"
"time"
"github.com/maximhq/bifrost/core/schemas"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// =============================================================================
// TOOL NAME CONFLICT TESTS
// =============================================================================
// TestToolNameConflict_MultipleClients - FULLY IMPLEMENTED EXAMPLE
func TestToolNameConflict_MultipleClients(t *testing.T) {
t.Parallel()
config := GetTestConfig(t)
if config.HTTPServerURL == "" {
t.Skip("MCP_HTTP_URL not set")
}
// Create two clients with same tool
client1Config := GetSampleHTTPClientConfig(config.HTTPServerURL)
client1Config.ID = "client-1"
client1Config.Name = "Client 1"
client2Config := GetSampleHTTPClientConfig(config.HTTPServerURL)
client2Config.ID = "client-2"
client2Config.Name = "Client 2"
manager := setupMCPManager(t, client1Config, client2Config)
bifrost := setupBifrost(t)
bifrost.SetMCPManager(manager)
// Execute a tool that exists on both clients
ctx := createTestContext()
toolCall := GetSampleEchoToolCall("call-1", "test")
result, bifrostErr := bifrost.ExecuteChatMCPTool(ctx, &toolCall)
// Should succeed (picks one of the clients)
if bifrostErr == nil {
t.Logf("Tool executed successfully, picked client: %v", result)
// Check ExtraFields to see which client was selected
} else {
t.Logf("Tool execution failed: %v", bifrostErr)
}
}
func TestToolNameConflict_Resolution(t *testing.T) {
t.Parallel()
// Setup in-process client
clientConfig := GetSampleInProcessClientConfig()
clientConfig.ToolsToExecute = []string{"*"}
manager := setupMCPManager(t, clientConfig)
// Register echo tool - will be available as bifrostInternal-echo
require.NoError(t, RegisterEchoTool(manager))
bifrost := setupBifrost(t)
bifrost.SetMCPManager(manager)
ctx := createTestContext()
// Execute "echo" tool multiple times to verify consistent execution
for i := 0; i < 10; i++ {
toolCall := GetSampleEchoToolCall("call-"+string(rune(i)), "test conflict resolution")
result, bifrostErr := bifrost.ExecuteChatMCPTool(ctx, &toolCall)
require.Nil(t, bifrostErr, "tool should execute")
require.NotNil(t, result)
t.Logf("Execution %d completed", i+1)
}
t.Log("✓ Tool executed consistently across multiple calls")
}
func TestToolNameConflict_WithFiltering(t *testing.T) {
t.Parallel()
config := GetTestConfig(t)
if config.HTTPServerURL == "" || config.SSEServerURL == "" {
t.Skip("MCP_HTTP_SERVER_URL or MCP_SSE_URL not set")
}
// Client 1: has "echo" tool, ToolsToExecute = ["echo"]
client1 := GetSampleHTTPClientConfig(config.HTTPServerURL)
client1.ID = "http-allow-echo"
client1.ToolsToExecute = []string{"echo"}
// Client 2: has "echo" tool, ToolsToExecute = [] (deny all)
client2 := GetSampleSSEClientConfig(config.SSEServerURL)
client2.ID = "sse-deny-all"
client2.ToolsToExecute = []string{} // Deny all
manager := setupMCPManager(t, client1, client2)
// Register the echo tool for bifrostInternal
require.NoError(t, RegisterEchoTool(manager))
bifrost := setupBifrost(t)
bifrost.SetMCPManager(manager)
ctx := createTestContext()
// Execute "echo" - should use bifrostInternal client (it's the only one with the tool registered in-process)
toolCall := GetSampleEchoToolCall("call-1", "filtered conflict")
result, bifrostErr := bifrost.ExecuteChatMCPTool(ctx, &toolCall)
err := bifrostErr
require.Nil(t, err, "should execute echo tool")
require.NotNil(t, result)
// Verify it executed successfully
// Note: ExecuteChatMCPTool doesn't return ExtraFields, so we can't verify client name
// The tool execution succeeded, which is what we're testing
}
func TestToolNameConflict_LocalVsExternal(t *testing.T) {
t.Parallel()
config := GetTestConfig(t)
if config.HTTPServerURL == "" {
t.Skip("MCP_HTTP_URL not set")
}
// Create HTTP client with "echo" tool (external)
httpClient := GetSampleHTTPClientConfig(config.HTTPServerURL)
httpClient.ID = "http-external"
httpClient.ToolsToExecute = []string{"*"}
// Create InProcess client and will register "echo" tool (local)
inProcessClient := GetSampleInProcessClientConfig()
inProcessClient.ID = "inprocess-local"
inProcessClient.ToolsToExecute = []string{"*"}
manager := setupMCPManager(t, httpClient, inProcessClient)
bifrost := setupBifrost(t)
bifrost.SetMCPManager(manager)
// Register "echo" tool in InProcess client
echoTool := GetSampleEchoTool()
echoToolHandler := func(args any) (string, error) {
argsMap, ok := args.(map[string]interface{})
if !ok {
return "", fmt.Errorf("invalid arguments type")
}
message, ok := argsMap["message"].(string)
if !ok {
return "", fmt.Errorf("message is required")
}
return message, nil
}
err := manager.RegisterTool("echo", "Local echo tool", echoToolHandler, echoTool)
require.NoError(t, err, "should register local echo tool")
ctx := createTestContext()
// Execute "echo" - verify which takes priority
toolCall := GetSampleEchoToolCall("call-1", "local vs external")
result, bifrostErr := bifrost.ExecuteChatMCPTool(ctx, &toolCall)
require.Nil(t, bifrostErr, "tool should execute")
require.NotNil(t, result)
// Check which client was used
// Note: ExecuteChatMCPTool doesn't return ExtraFields, so we can't get client name
t.Logf("Tool execution completed")
// Priority behavior cannot be verified without ExtraFields
}
// =============================================================================
// MULTIPLE SAME-NAME TOOLS TESTS
// =============================================================================
func TestMultipleSameNameTools_ThreeClients(t *testing.T) {
t.Parallel()
// Create in-process client with "calculator" tool
client := GetSampleInProcessClientConfig()
client.ToolsToExecute = []string{"*"}
manager := setupMCPManager(t, client)
// Register calculator tool - will be available as bifrostInternal-calculator
require.NoError(t, RegisterCalculatorTool(manager))
bifrost := setupBifrost(t)
bifrost.SetMCPManager(manager)
ctx := createTestContext()
// Execute calculator multiple times
for i := 0; i < 15; i++ {
toolCall := GetSampleCalculatorToolCall("call-"+string(rune(i)), "add", float64(i), 1.0)
result, bifrostErr := bifrost.ExecuteChatMCPTool(ctx, &toolCall)
require.Nil(t, bifrostErr, "calculator should execute")
require.NotNil(t, result)
}
// Verify only the bifrostInternal client is active (disconnected clients may be
// retained in memory for auto-recovery and should not be counted here)
clients := manager.GetClients()
activeClients := 0
for _, c := range clients {
if c.State != schemas.MCPConnectionStateDisconnected {
activeClients++
}
}
assert.Equal(t, 1, activeClients, "should have 1 bifrostInternal client")
t.Log("✓ Calculator executed successfully across 15 calls")
}
func TestMultipleSameNameTools_DifferentImplementations(t *testing.T) {
t.Parallel()
// Use bifrostInternal client only with registered tools
inProcessClient := GetSampleInProcessClientConfig()
inProcessClient.ID = "inprocess-custom"
inProcessClient.ToolsToExecute = []string{"*"}
manager := setupMCPManager(t, inProcessClient)
bifrost := setupBifrost(t)
bifrost.SetMCPManager(manager)
// Register custom "process_data" tool in InProcess client
processTool := schemas.ChatTool{
Type: schemas.ChatToolTypeFunction,
Function: &schemas.ChatToolFunction{
Name: "process_data",
Description: schemas.Ptr("Custom data processor"),
Parameters: &schemas.ToolFunctionParameters{
Type: "object",
Properties: schemas.NewOrderedMapFromPairs(
schemas.KV("data", map[string]interface{}{
"type": "string",
"description": "Data to process",
}),
),
Required: []string{"data"},
},
},
}
processToolHandler := func(args any) (string, error) {
argsMap, ok := args.(map[string]interface{})
if !ok {
return "", fmt.Errorf("invalid arguments type")
}
data, ok := argsMap["data"].(string)
if !ok {
return "", fmt.Errorf("data is required")
}
return "Processed: " + data, nil
}
err := manager.RegisterTool("process_data", "Custom data processor", processToolHandler, processTool)
require.Nil(t, err)
ctx := createTestContext()
// Execute "process_data" tool multiple times
for i := 0; i < 5; i++ {
toolCall := schemas.ChatAssistantMessageToolCall{
ID: schemas.Ptr("call-" + string(rune(i))),
Type: schemas.Ptr("function"),
Function: schemas.ChatAssistantMessageToolCallFunction{
Name: schemas.Ptr("bifrostInternal-process_data"),
Arguments: `{"data": "test"}`,
},
}
result, bifrostErr := bifrost.ExecuteChatMCPTool(ctx, &toolCall)
require.Nil(t, bifrostErr, "process_data should execute")
require.NotNil(t, result)
}
t.Log("✓ Custom tool execution completed successfully")
}
// =============================================================================
// CONFLICT WITH CLIENT STATES
// =============================================================================
func TestToolConflict_OneClientDisconnected(t *testing.T) {
t.Parallel()
// Client 1: connected in-process client
client1 := GetSampleInProcessClientConfig()
client1.ID = "connected-client"
client1.ToolsToExecute = []string{"*"}
// Client 2: disconnected (bad config to simulate disconnect)
client2 := GetSampleHTTPClientConfig("http://localhost:1")
client2.ID = "disconnected-client"
client2.ToolsToExecute = []string{"*"}
manager := setupMCPManager(t, client1, client2)
// Register echo tool - will be available as bifrostInternal-echo on client1
require.NoError(t, RegisterEchoTool(manager))
bifrost := setupBifrost(t)
bifrost.SetMCPManager(manager)
// Wait a bit for client2 to fail connection
time.Sleep(2 * time.Second)
// Verify only bifrostInternal is active (disconnected clients may be retained
// in memory for auto-recovery but should not be counted as active)
clients := manager.GetClients()
activeClients := 0
for _, c := range clients {
if c.State != schemas.MCPConnectionStateDisconnected {
activeClients++
}
}
require.Equal(t, 1, activeClients, "should only have 1 active client (bifrostInternal)")
ctx := createTestContext()
// Execute "echo" - should use Client 1 (only connected one)
toolCall := GetSampleEchoToolCall("call-1", "disconnected conflict")
result, bifrostErr := bifrost.ExecuteChatMCPTool(ctx, &toolCall)
require.Nil(t, bifrostErr, "should use connected client")
require.NotNil(t, result)
t.Log("✓ Tool executed successfully using the connected client")
}
func TestToolConflict_BothClientsDisconnected(t *testing.T) {
t.Parallel()
// Both clients have "echo" but both are disconnected
// Using bad URLs to force disconnect
client1 := GetSampleHTTPClientConfig("http://localhost:1")
client1.ID = "disconnected-1"
client1.ToolsToExecute = []string{"*"}
client2 := GetSampleHTTPClientConfig("http://localhost:2")
client2.ID = "disconnected-2"
client2.ToolsToExecute = []string{"*"}
manager := setupMCPManager(t, client1, client2)
bifrost := setupBifrost(t)
bifrost.SetMCPManager(manager)
// Wait for both to fail connection
time.Sleep(2 * time.Second)
// Verify both are disconnected
clients := manager.GetClients()
for _, client := range clients {
t.Logf("Client %s state: %v", client.ExecutionConfig.ID, client.State)
}
ctx := createTestContext()
// Execute "echo" - should return error (no available client)
toolCall := GetSampleEchoToolCall("call-1", "all disconnected")
result, bifrostErr := bifrost.ExecuteChatMCPTool(ctx, &toolCall)
// Should fail because no client is available
assert.NotNil(t, bifrostErr, "should fail when all clients are disconnected")
assert.Nil(t, result)
if bifrostErr != nil && bifrostErr.Error != nil {
errorMsg := bifrostErr.Error.Message
// Error message can be "not found", "not available", or "not permitted"
hasExpectedError := assert.True(t,
strings.Contains(errorMsg, "not available") || strings.Contains(errorMsg, "not permitted") || strings.Contains(errorMsg, "not found"),
"error should mention tool is not available/permitted/found, got: %s", errorMsg)
_ = hasExpectedError
}
}
// =============================================================================
// BOTH API FORMATS CONFLICT TESTS
// =============================================================================
func TestToolConflict_ChatFormat(t *testing.T) {
t.Parallel()
// Use bifrostInternal clients with registered tools
inProcessClient := GetSampleInProcessClientConfig()
inProcessClient.ID = "inprocess-client"
inProcessClient.ToolsToExecute = []string{"*"}
manager := setupMCPManager(t, inProcessClient)
// Register all tools needed for this test
require.NoError(t, RegisterEchoTool(manager))
require.NoError(t, RegisterCalculatorTool(manager))
require.NoError(t, RegisterWeatherTool(manager))
bifrost := setupBifrost(t)
bifrost.SetMCPManager(manager)
ctx := createTestContext()
// Execute in Chat format
testCases := []struct {
name string
toolCall schemas.ChatAssistantMessageToolCall
}{
{
name: "echo_tool",
toolCall: GetSampleEchoToolCall("call-echo", "chat format conflict"),
},
{
name: "calculator_tool",
toolCall: GetSampleCalculatorToolCall("call-calc", "multiply", 7, 8),
},
{
name: "weather_tool",
toolCall: GetSampleWeatherToolCall("call-weather", "London", ""),
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result, bifrostErr := bifrost.ExecuteChatMCPTool(ctx, &tc.toolCall)
if bifrostErr != nil && bifrostErr.Error != nil {
fmt.Println("bifrostErr", bifrostErr.Error.Message)
}
require.Nil(t, bifrostErr, "should resolve conflict and execute")
require.NotNil(t, result)
// Log which client was selected
// Note: ExecuteChatMCPTool doesn't return ExtraFields
t.Logf("Tool %s executed successfully", *tc.toolCall.Function.Name)
})
}
}
func TestToolConflict_ResponsesFormat(t *testing.T) {
t.Parallel()
// Use bifrostInternal clients with registered tools
inProcessClient := GetSampleInProcessClientConfig()
inProcessClient.ID = "inprocess-client"
inProcessClient.ToolsToExecute = []string{"*"}
manager := setupMCPManager(t, inProcessClient)
// Register all tools needed for this test
require.NoError(t, RegisterEchoTool(manager))
require.NoError(t, RegisterCalculatorTool(manager))
require.NoError(t, RegisterWeatherTool(manager))
bifrost := setupBifrost(t)
bifrost.SetMCPManager(manager)
ctx := createTestContext()
// Execute in Responses format
testCases := []struct {
name string
responsesToolMsg schemas.ResponsesToolMessage
}{
{
name: "echo_tool",
responsesToolMsg: schemas.ResponsesToolMessage{
CallID: schemas.Ptr("call-echo"),
Name: schemas.Ptr("bifrostInternal-echo"),
Arguments: schemas.Ptr(`{"message": "responses format conflict"}`),
},
},
{
name: "calculator_tool",
responsesToolMsg: schemas.ResponsesToolMessage{
CallID: schemas.Ptr("call-calc"),
Name: schemas.Ptr("bifrostInternal-calculator"),
Arguments: schemas.Ptr(`{"operation": "add", "x": 15, "y": 25}`),
},
},
{
name: "weather_tool",
responsesToolMsg: schemas.ResponsesToolMessage{
CallID: schemas.Ptr("call-weather"),
Name: schemas.Ptr("bifrostInternal-get_weather"),
Arguments: schemas.Ptr(`{"location": "Tokyo"}`),
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result, bifrostErr := bifrost.ExecuteResponsesMCPTool(ctx, &tc.responsesToolMsg)
require.Nil(t, bifrostErr, "should resolve conflict and execute")
require.NotNil(t, result)
// Log which client was selected
// Note: ExecuteResponsesMCPTool doesn't return ExtraFields
t.Logf("Tool %s executed successfully", *tc.responsesToolMsg.Name)
})
}
}
// =============================================================================
// COMPREHENSIVE CONFLICT SCENARIOS
// =============================================================================
func TestToolConflict_ComprehensiveScenarios(t *testing.T) {
t.Parallel()
// Use bifrostInternal clients with registered tools
inProcessClient := GetSampleInProcessClientConfig()
inProcessClient.ID = "inprocess-client"
inProcessClient.ToolsToExecute = []string{"*"}
manager := setupMCPManager(t, inProcessClient)
// Register all tools needed for this test
require.NoError(t, RegisterEchoTool(manager))
require.NoError(t, RegisterCalculatorTool(manager))
require.NoError(t, RegisterWeatherTool(manager))
bifrost := setupBifrost(t)
bifrost.SetMCPManager(manager)
ctx := createTestContext()
scenarios := []struct {
name string
toolName string
expectSuccess bool
}{
{
name: "echo_tool",
toolName: "bifrostInternal-echo",
expectSuccess: true,
},
{
name: "calculator_tool",
toolName: "bifrostInternal-calculator",
expectSuccess: true,
},
{
name: "weather_tool",
toolName: "bifrostInternal-get_weather",
expectSuccess: true,
},
}
for _, scenario := range scenarios {
t.Run(scenario.name, func(t *testing.T) {
toolCall := GetSampleEchoToolCall("call-"+scenario.toolName, "test")
toolCall.Function.Name = schemas.Ptr(scenario.toolName)
result, bifrostErr := bifrost.ExecuteChatMCPTool(ctx, &toolCall)
if scenario.expectSuccess {
require.Nil(t, bifrostErr, "should execute successfully")
require.NotNil(t, result)
t.Logf("Tool %s executed successfully", scenario.toolName)
} else {
assert.NotNil(t, bifrostErr, "should fail")
}
})
}
}