mirror of
https://github.com/httprunner/httprunner.git
synced 2026-05-07 05:42:46 +08:00
455 lines
11 KiB
Go
455 lines
11 KiB
Go
package mcphost
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/mark3labs/mcp-go/mcp"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestConvertToolsToRecordsFromFile(t *testing.T) {
|
|
hub, err := NewMCPHost("./testdata/test.mcp.json", true)
|
|
require.NoError(t, err)
|
|
|
|
// use ExportToolsToJSON to dump tools to JSON file
|
|
err = hub.ExportToolsToJSON(context.Background(), "./tools_records.json")
|
|
require.NoError(t, err)
|
|
|
|
// read the exported JSON file
|
|
data, err := os.ReadFile("./tools_records.json")
|
|
require.NoError(t, err)
|
|
|
|
// parse the exported JSON data
|
|
var records []MCPToolRecord
|
|
err = json.Unmarshal(data, &records)
|
|
require.NoError(t, err)
|
|
|
|
// verify the number of records
|
|
assert.NotEmpty(t, records, "Exported records should not be empty")
|
|
|
|
t.Logf("Tools records written to ./tools_records.json")
|
|
}
|
|
|
|
func TestExtractDocStringInfo(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
docstring string
|
|
want DocStringInfo
|
|
}{
|
|
{
|
|
name: "complete docstring with args and returns",
|
|
docstring: `Get weather alerts for a US state.
|
|
|
|
Args:
|
|
state: Two-letter US state code (e.g. CA, NY)
|
|
|
|
Returns:
|
|
alerts: List of active weather alerts for the specified state
|
|
error: Error message if the request fails
|
|
`,
|
|
want: DocStringInfo{
|
|
Description: "Get weather alerts for a US state.",
|
|
Parameters: map[string]string{
|
|
"state": "Two-letter US state code (e.g. CA, NY)",
|
|
},
|
|
Returns: map[string]string{
|
|
"alerts": "List of active weather alerts for the specified state",
|
|
"error": "Error message if the request fails",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "docstring with only args",
|
|
docstring: `Do screen swipe action.
|
|
|
|
Args:
|
|
direction: swipe direction (up, down)
|
|
`,
|
|
want: DocStringInfo{
|
|
Description: "Do screen swipe action.",
|
|
Parameters: map[string]string{
|
|
"direction": "swipe direction (up, down)",
|
|
},
|
|
Returns: map[string]string{},
|
|
},
|
|
},
|
|
{
|
|
name: "docstring with only description",
|
|
docstring: "Simple tool with no parameters.",
|
|
want: DocStringInfo{
|
|
Description: "Simple tool with no parameters.",
|
|
Parameters: map[string]string{},
|
|
Returns: map[string]string{},
|
|
},
|
|
},
|
|
{
|
|
name: "docstring with multiple parameters",
|
|
docstring: `Perform complex operation.
|
|
|
|
Args:
|
|
param1: first parameter description
|
|
param2: second parameter description
|
|
param3: third parameter description
|
|
|
|
Returns:
|
|
result: operation result
|
|
`,
|
|
want: DocStringInfo{
|
|
Description: "Perform complex operation.",
|
|
Parameters: map[string]string{
|
|
"param1": "first parameter description",
|
|
"param2": "second parameter description",
|
|
"param3": "third parameter description",
|
|
},
|
|
Returns: map[string]string{
|
|
"result": "operation result",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := extractDocStringInfo(tt.docstring)
|
|
assert.Equal(t, tt.want.Description, got.Description)
|
|
assert.Equal(t, tt.want.Parameters, got.Parameters)
|
|
assert.Equal(t, tt.want.Returns, got.Returns)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestConvertToolsToRecords(t *testing.T) {
|
|
// Create a mock MCPHost for testing
|
|
host := &MCPHost{
|
|
connections: make(map[string]*Connection),
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
tools []MCPTools
|
|
want []MCPToolRecord
|
|
}{
|
|
{
|
|
name: "convert weather tool",
|
|
tools: []MCPTools{
|
|
{
|
|
ServerName: "weather",
|
|
Tools: []mcp.Tool{
|
|
{
|
|
Name: "get_alerts",
|
|
Description: `Get weather alerts for a US state.
|
|
|
|
Args:
|
|
state: Two-letter US state code (e.g. CA, NY)
|
|
|
|
Returns:
|
|
alerts: List of active weather alerts for the specified state
|
|
error: Error message if the request fails
|
|
`,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
want: []MCPToolRecord{
|
|
{
|
|
ToolID: "weather__get_alerts",
|
|
ServerName: "weather",
|
|
ToolName: "get_alerts",
|
|
Description: "Get weather alerts for a US state.",
|
|
Parameters: `{"state":"Two-letter US state code (e.g. CA, NY)"}`,
|
|
Returns: `{"alerts":"List of active weather alerts for the specified state","error":"Error message if the request fails"}`,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "convert multiple tools",
|
|
tools: []MCPTools{
|
|
{
|
|
ServerName: "ui",
|
|
Tools: []mcp.Tool{
|
|
{
|
|
Name: "swipe",
|
|
Description: `Do screen swipe action.
|
|
|
|
Args:
|
|
direction: swipe direction (up, down)
|
|
`,
|
|
},
|
|
{
|
|
Name: "tap",
|
|
Description: "Tap on screen at specified position.",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
want: []MCPToolRecord{
|
|
{
|
|
ToolID: "ui__swipe",
|
|
ServerName: "ui",
|
|
ToolName: "swipe",
|
|
Description: "Do screen swipe action.",
|
|
Parameters: `{"direction":"swipe direction (up, down)"}`,
|
|
Returns: "{}",
|
|
},
|
|
{
|
|
ToolID: "ui__tap",
|
|
ServerName: "ui",
|
|
ToolName: "tap",
|
|
Description: "Tap on screen at specified position.",
|
|
Parameters: "{}",
|
|
Returns: "{}",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "convert tool with InputSchema",
|
|
tools: []MCPTools{
|
|
{
|
|
ServerName: "test",
|
|
Tools: []mcp.Tool{
|
|
{
|
|
Name: "test_tool",
|
|
Description: "Test tool with input schema",
|
|
InputSchema: mcp.ToolInputSchema{
|
|
Type: "object",
|
|
Properties: map[string]interface{}{
|
|
"param1": map[string]interface{}{
|
|
"type": "string",
|
|
"description": "First parameter",
|
|
},
|
|
"param2": map[string]interface{}{
|
|
"type": "number",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
want: []MCPToolRecord{
|
|
{
|
|
ToolID: "test__test_tool",
|
|
ServerName: "test",
|
|
ToolName: "test_tool",
|
|
Description: "Test tool with input schema",
|
|
Parameters: `{"param1":"First parameter","param2":"Parameter of type number"}`,
|
|
Returns: "{}",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := host.ConvertToolsToRecords(tt.tools)
|
|
|
|
// Compare each record
|
|
require.Equal(t, len(tt.want), len(got))
|
|
for i := range tt.want {
|
|
assert.Equal(t, tt.want[i].ToolID, got[i].ToolID)
|
|
assert.Equal(t, tt.want[i].ServerName, got[i].ServerName)
|
|
assert.Equal(t, tt.want[i].ToolName, got[i].ToolName)
|
|
assert.Equal(t, tt.want[i].Description, got[i].Description)
|
|
|
|
// Compare JSON content (ignoring whitespace differences)
|
|
var wantParams, gotParams, wantReturns, gotReturns map[string]string
|
|
require.NoError(t, json.Unmarshal([]byte(tt.want[i].Parameters), &wantParams))
|
|
require.NoError(t, json.Unmarshal([]byte(got[i].Parameters), &gotParams))
|
|
require.NoError(t, json.Unmarshal([]byte(tt.want[i].Returns), &wantReturns))
|
|
require.NoError(t, json.Unmarshal([]byte(got[i].Returns), &gotReturns))
|
|
|
|
assert.Equal(t, wantParams, gotParams)
|
|
assert.Equal(t, wantReturns, gotReturns)
|
|
|
|
// Verify timestamps are recent (within last 5 seconds)
|
|
now := time.Now()
|
|
assert.True(t, now.Sub(got[i].CreatedAt) < 5*time.Second, "CreatedAt should be recent")
|
|
assert.True(t, now.Sub(got[i].LastUpdatedAt) < 5*time.Second, "LastUpdatedAt should be recent")
|
|
// CreatedAt and LastUpdatedAt should be the same
|
|
assert.Equal(t, got[i].CreatedAt, got[i].LastUpdatedAt)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestExtractParameters tests the extractParameters method
|
|
func TestExtractParameters(t *testing.T) {
|
|
host := &MCPHost{}
|
|
|
|
tests := []struct {
|
|
name string
|
|
tool mcp.Tool
|
|
info DocStringInfo
|
|
expected string
|
|
}{
|
|
{
|
|
name: "extract from InputSchema",
|
|
tool: mcp.Tool{
|
|
InputSchema: mcp.ToolInputSchema{
|
|
Properties: map[string]interface{}{
|
|
"param1": map[string]interface{}{
|
|
"type": "string",
|
|
"description": "First parameter",
|
|
},
|
|
"param2": map[string]interface{}{
|
|
"type": "number",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
info: DocStringInfo{Parameters: map[string]string{"old": "old param"}},
|
|
expected: `{"param1":"First parameter","param2":"Parameter of type number"}`,
|
|
},
|
|
{
|
|
name: "fallback to docstring",
|
|
tool: mcp.Tool{},
|
|
info: DocStringInfo{
|
|
Parameters: map[string]string{
|
|
"param": "parameter description",
|
|
},
|
|
},
|
|
expected: `{"param":"parameter description"}`,
|
|
},
|
|
{
|
|
name: "empty parameters",
|
|
tool: mcp.Tool{},
|
|
info: DocStringInfo{},
|
|
expected: "{}",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := host.extractParameters(tt.tool, tt.info)
|
|
assert.Equal(t, tt.expected, got)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestExtractReturns tests the extractReturns method
|
|
func TestExtractReturns(t *testing.T) {
|
|
host := &MCPHost{
|
|
connections: make(map[string]*Connection),
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
serverName string
|
|
toolName string
|
|
info DocStringInfo
|
|
expected string
|
|
}{
|
|
{
|
|
name: "fallback to docstring returns",
|
|
serverName: "unknown_server",
|
|
toolName: "unknown_tool",
|
|
info: DocStringInfo{
|
|
Returns: map[string]string{
|
|
"result": "operation result",
|
|
"error": "error message",
|
|
},
|
|
},
|
|
expected: `{"error":"error message","result":"operation result"}`,
|
|
},
|
|
{
|
|
name: "empty returns",
|
|
serverName: "unknown_server",
|
|
toolName: "unknown_tool",
|
|
info: DocStringInfo{},
|
|
expected: "{}",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := host.extractReturns(tt.serverName, tt.toolName, tt.info)
|
|
assert.Equal(t, tt.expected, got)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestGetPropertyDescription tests the getPropertyDescription method
|
|
func TestGetPropertyDescription(t *testing.T) {
|
|
host := &MCPHost{}
|
|
|
|
tests := []struct {
|
|
name string
|
|
propMap map[string]interface{}
|
|
expected string
|
|
}{
|
|
{
|
|
name: "with description",
|
|
propMap: map[string]interface{}{
|
|
"type": "string",
|
|
"description": "Parameter description",
|
|
},
|
|
expected: "Parameter description",
|
|
},
|
|
{
|
|
name: "without description, with type",
|
|
propMap: map[string]interface{}{
|
|
"type": "number",
|
|
},
|
|
expected: "Parameter of type number",
|
|
},
|
|
{
|
|
name: "without description and type",
|
|
propMap: map[string]interface{}{},
|
|
expected: "Parameter",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := host.getPropertyDescription(tt.propMap)
|
|
assert.Equal(t, tt.expected, got)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestMarshalToJSON tests the marshalToJSON method
|
|
func TestMarshalToJSON(t *testing.T) {
|
|
host := &MCPHost{}
|
|
|
|
tests := []struct {
|
|
name string
|
|
data interface{}
|
|
dataType string
|
|
expected string
|
|
}{
|
|
{
|
|
name: "valid map",
|
|
data: map[string]string{
|
|
"key1": "value1",
|
|
"key2": "value2",
|
|
},
|
|
dataType: "test data",
|
|
expected: `{"key1":"value1","key2":"value2"}`,
|
|
},
|
|
{
|
|
name: "empty map",
|
|
data: map[string]string{},
|
|
dataType: "test data",
|
|
expected: "{}",
|
|
},
|
|
{
|
|
name: "invalid data (channel)",
|
|
data: make(chan int),
|
|
dataType: "test data",
|
|
expected: "{}",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := host.marshalToJSON(tt.data, tt.dataType)
|
|
assert.Equal(t, tt.expected, got)
|
|
})
|
|
}
|
|
}
|