package engine import ( "context" "encoding/json" "strings" "testing" "git.flytoex.net/yuanwei/flyto-agent/internal/mcp" "git.flytoex.net/yuanwei/flyto-agent/pkg/execenv" ) func TestPluginMCPServerKey_RoundTrip(t *testing.T) { cases := []struct { plugin string server string }{ {"foo", "bar"}, {"my-plugin", "db-server"}, {"p1", "s_with_underscore"}, {"a", "b.with.dots"}, // server with dots: only first dot is separator } for _, c := range cases { key := pluginMCPServerKey(c.plugin, c.server) if !strings.HasPrefix(key, pluginMCPServerKeyPrefix) { t.Errorf("key %q missing prefix", key) } p, s, ok := parsePluginMCPServerKey(key) if !ok { t.Errorf("parse %q failed", key) continue } if p != c.plugin || s != c.server { t.Errorf("round-trip mismatch: got (%q,%q), want (%q,%q)", p, s, c.plugin, c.server) } } } func TestParsePluginMCPServerKey_Foreign(t *testing.T) { // Keys without the plugin prefix must be rejected so // user-configured MCP servers are left alone. cases := []string{"", "user-server", "random.name", "plugin_no_dot_after_prefix"} for _, k := range cases { if _, _, ok := parsePluginMCPServerKey(k); ok { t.Errorf("parsePluginMCPServerKey(%q) should have returned ok=false", k) } } } func TestMcpProxyToolName_Format(t *testing.T) { got := mcpProxyToolName("myplug", "gh", "list_issues") want := "myplug:gh/list_issues" if got != want { t.Errorf("mcpProxyToolName = %q, want %q", got, want) } } func TestMcpResultToToolResult_Text(t *testing.T) { r := &mcp.ToolCallResult{ Content: []mcp.ContentItem{ {Type: "text", Text: "hello"}, {Type: "text", Text: "world"}, }, } out := mcpResultToToolResult(r) if out.IsError { t.Errorf("unexpected IsError=true") } if out.Output != "hello\nworld" { t.Errorf("Output = %q, want %q", out.Output, "hello\nworld") } } func TestMcpResultToToolResult_ErrorFlag(t *testing.T) { r := &mcp.ToolCallResult{ Content: []mcp.ContentItem{{Type: "text", Text: "boom"}}, IsError: true, } out := mcpResultToToolResult(r) if !out.IsError { t.Errorf("IsError not propagated") } } func TestMcpResultToToolResult_Nil(t *testing.T) { out := mcpResultToToolResult(nil) if out == nil || out.Output != "" || out.IsError { t.Errorf("nil input should yield empty Result, got %+v", out) } } func TestMcpResultToToolResult_MixedContent(t *testing.T) { r := &mcp.ToolCallResult{ Content: []mcp.ContentItem{ {Type: "text", Text: "before"}, {Type: "image", MimeType: "image/png", Data: "abcd"}, {Type: "resource", URI: "file:///tmp/x"}, {Type: "mystery"}, }, } out := mcpResultToToolResult(r) if !strings.Contains(out.Output, "before") { t.Errorf("missing text content: %q", out.Output) } if !strings.Contains(out.Output, "image/png") { t.Errorf("missing image annotation: %q", out.Output) } if !strings.Contains(out.Output, "file:///tmp/x") { t.Errorf("missing resource annotation: %q", out.Output) } if !strings.Contains(out.Output, "mystery") { t.Errorf("missing unknown-type annotation: %q", out.Output) } } func TestMcpProxyTool_InputSchema_FallsBackToEmptyObject(t *testing.T) { p := &mcpProxyTool{name: "p:s/t"} schema := p.InputSchema() if !strings.Contains(string(schema), `"object"`) { t.Errorf("empty schema fallback should describe object type, got %q", schema) } } func TestMcpProxyTool_Execute_InvalidJSON(t *testing.T) { // Intentionally bad input: valid JSON but not an object, so Unmarshal // into map[string]any fails. Execute must return an IsError // Result (so the agent self-corrects), not a Go error. p := &mcpProxyTool{name: "p:s/t", mgr: mcp.NewManager(execenv.DefaultExecutor{})} result, err := p.Execute(context.Background(), json.RawMessage(`"not an object"`), nil) if err != nil { t.Errorf("unexpected go error: %v", err) } if result == nil || !result.IsError { t.Errorf("expected IsError Result, got %+v", result) } }