// bridge_test.go - MCPToolBridge 和 normalizeInputSchema 测试. package mcp import ( "context" "encoding/json" "testing" ) // ── normalizeInputSchema 测试 ───────────────────────────────────────────────── func TestNormalizeInputSchema_AddsMissingType(t *testing.T) { input := json.RawMessage(`{"properties":{"a":{"type":"string"}}}`) out := normalizeInputSchema(input) var schema map[string]json.RawMessage if err := json.Unmarshal(out, &schema); err != nil { t.Fatalf("unmarshal result: %v", err) } var typ string if err := json.Unmarshal(schema["type"], &typ); err != nil || typ != "object" { t.Errorf("type = %q, want \"object\"", typ) } } func TestNormalizeInputSchema_PreservesExistingType(t *testing.T) { input := json.RawMessage(`{"type":"array","items":{"type":"string"}}`) out := normalizeInputSchema(input) var schema map[string]json.RawMessage json.Unmarshal(out, &schema) var typ string json.Unmarshal(schema["type"], &typ) if typ != "array" { t.Errorf("type = %q, want \"array\"", typ) } } func TestNormalizeInputSchema_PreservesOneOf(t *testing.T) { input := json.RawMessage(`{"oneOf":[{"type":"string"},{"type":"number"}]}`) out := normalizeInputSchema(input) var schema map[string]json.RawMessage json.Unmarshal(out, &schema) // 有 oneOf 时不应添加 type if _, hasType := schema["type"]; hasType { t.Error("schema with oneOf should not have type injected") } if _, hasOneOf := schema["oneOf"]; !hasOneOf { t.Error("oneOf should be preserved") } } func TestNormalizeInputSchema_PreservesAnyOf(t *testing.T) { input := json.RawMessage(`{"anyOf":[{"type":"string"},{"type":"null"}]}`) out := normalizeInputSchema(input) var schema map[string]json.RawMessage json.Unmarshal(out, &schema) if _, hasType := schema["type"]; hasType { t.Error("schema with anyOf should not have type injected") } } func TestNormalizeInputSchema_InvalidJSON_Passthrough(t *testing.T) { input := json.RawMessage(`not json`) out := normalizeInputSchema(input) // 无效 JSON 应原样返回 if string(out) != "not json" { t.Errorf("invalid JSON should pass through unchanged, got %q", out) } } func TestNormalizeInputSchema_EmptyObject(t *testing.T) { input := json.RawMessage(`{}`) out := normalizeInputSchema(input) var schema map[string]json.RawMessage json.Unmarshal(out, &schema) var typ string json.Unmarshal(schema["type"], &typ) if typ != "object" { t.Errorf("empty schema type = %q, want \"object\"", typ) } } // ── MCPToolBridge 测试 ──────────────────────────────────────────────────────── func TestNewMCPToolBridge_FullName(t *testing.T) { tool := MCPTool{Name: "search_code", Description: "Search code"} bridge := NewMCPToolBridge(tool, "github", nil) if bridge.Name() != "mcp__github__search_code" { t.Errorf("Name() = %q, want %q", bridge.Name(), "mcp__github__search_code") } } func TestMCPToolBridge_Description_WithDesc(t *testing.T) { tool := MCPTool{Name: "read", Description: "Read a file"} bridge := NewMCPToolBridge(tool, "fs", nil) desc := bridge.Description(context.Background()) if desc != "Read a file (via MCP: fs)" { t.Errorf("Description = %q", desc) } } func TestMCPToolBridge_Description_NoDesc(t *testing.T) { tool := MCPTool{Name: "read", Description: ""} bridge := NewMCPToolBridge(tool, "fs", nil) desc := bridge.Description(context.Background()) if desc == "" { t.Error("Description should not be empty even when tool.Description is ''") } } func TestMCPToolBridge_InputSchema_Nil(t *testing.T) { tool := MCPTool{Name: "t", InputSchema: nil} bridge := NewMCPToolBridge(tool, "s", nil) schema := bridge.InputSchema() if string(schema) != `{"type":"object","properties":{}}` { t.Errorf("InputSchema with nil = %q", schema) } } func TestMCPToolBridge_InputSchema_HasSchema(t *testing.T) { raw := json.RawMessage(`{"type":"object","properties":{"q":{"type":"string"}}}`) tool := MCPTool{Name: "search", InputSchema: raw} bridge := NewMCPToolBridge(tool, "github", nil) schema := bridge.InputSchema() var parsed map[string]json.RawMessage if err := json.Unmarshal(schema, &parsed); err != nil { t.Fatalf("InputSchema is invalid JSON: %v", err) } } func TestMCPToolBridge_Metadata(t *testing.T) { tool := MCPTool{Name: "search"} bridge := NewMCPToolBridge(tool, "github", nil) meta := bridge.Metadata() if meta.ConcurrencySafe { t.Error("MCP tools should not be ConcurrencySafe by default") } } // ── convertToolResult 测试 ──────────────────────────────────────────────────── func TestConvertToolResult_Nil(t *testing.T) { got := convertToolResult(nil) if got != "(no output)" { t.Errorf("convertToolResult(nil) = %q, want \"(no output)\"", got) } } func TestConvertToolResult_EmptyContent(t *testing.T) { got := convertToolResult(&ToolCallResult{Content: nil}) if got != "(no output)" { t.Errorf("convertToolResult(empty) = %q, want \"(no output)\"", got) } } func TestConvertToolResult_TextContent(t *testing.T) { result := &ToolCallResult{ Content: []ContentItem{ {Type: "text", Text: "hello"}, {Type: "text", Text: "world"}, }, } got := convertToolResult(result) if got != "hello\nworld" { t.Errorf("convertToolResult(text) = %q, want %q", got, "hello\nworld") } } func TestConvertToolResult_ImageContent(t *testing.T) { result := &ToolCallResult{ Content: []ContentItem{ {Type: "image", MimeType: "image/png"}, }, } got := convertToolResult(result) if got != "[image: image/png]" { t.Errorf("convertToolResult(image) = %q", got) } } func TestConvertToolResult_ResourceContent(t *testing.T) { result := &ToolCallResult{ Content: []ContentItem{ {Type: "resource", URI: "file:///test.txt"}, }, } got := convertToolResult(result) if got != "[resource: file:///test.txt]" { t.Errorf("convertToolResult(resource) = %q", got) } } func TestConvertToolResult_NoTextParts(t *testing.T) { result := &ToolCallResult{ Content: []ContentItem{ {Type: "unknown_type"}, }, } got := convertToolResult(result) if got != "(no text output)" { t.Errorf("convertToolResult(no-text) = %q, want \"(no text output)\"", got) } }