package mcp import ( "context" "testing" "git.yeij.top/AskaEth/Cyrene_v2/internal/capability/registry" "git.yeij.top/AskaEth/Cyrene_v2/pkg/plugins/sdk" ) // 本文件钉住 MCP 工具的**来源章**(DevDocs/audit/MCP工具按上游分组-2026-10-24.md): // 每把注册进注册表的工具都要带「哪台 server + 什么传输」,供面板按 server 分节。 // TestRegistryRegisterStampsToolOrigin 覆盖**两条接入路径**的共同汇点: // - 运行时热装:plugin.Manager.start → mcp.NewRegistry → **Register**; // - `.env` 的 MCP_SERVERS_JSON 启动期装:cmd/cyrene.registerMCPServer → // mcp.NewRegistry → **同一个 Register**(不经插件管理器)。 // // 两条路都在这里进注册表,因此这一处的断言就同时钉住了它们;也从反面证明 // 「从插件管理器现场反推归属」会漏掉 env 路径。 func TestRegistryRegisterStampsToolOrigin(t *testing.T) { reg := registry.New() cfg := MCPConfig{Name: "calc", TransportType: "stdio"} r := NewRegistry(reg, cfg, WithTransport(startFakeServer(t, newFakeServer()))) if err := r.Register(context.Background()); err != nil { t.Fatalf("Register: %v", err) } t.Cleanup(func() { _ = r.Unregister(context.Background()) }) want := sdk.Origin{Kind: sdk.OriginKindMCP, Server: "calc", Transport: "stdio"} tools := reg.List(registry.KindTool) if len(tools) != 2 { t.Fatalf("注册进注册表的工具数 = %d, want 2(echo/add)", len(tools)) } for _, cap := range tools { if cap.Origin != want { t.Fatalf("工具 %q 的来源章 = %+v, want %+v", cap.Name, cap.Origin, want) } } // 来源是 server 名,不是注册键 —— 与 `mcp:calc` 那条 KindMCP 条目无关。 if err := r.Unregister(context.Background()); err != nil { t.Fatalf("Unregister: %v", err) } if left := reg.List(registry.KindTool); len(left) != 0 { t.Fatalf("停用 server 后还剩 %d 把工具:条目必须连同来源章一起消失,不留孤儿", len(left)) } } // TestTransportNameOf 钉住来源章里 transport 的取值:它必须报**实际会用的**传输, // 而不是原样下发可能为空的 TransportType(配置常见留空,由推断决定)。 // 判据与 newTransport 的推断同源,因此面板显示的传输永远是真在用的那个。 func TestTransportNameOf(t *testing.T) { cases := []struct { name string cfg MCPConfig want string }{ {"显式 stdio", MCPConfig{TransportType: "stdio"}, "stdio"}, {"显式 sse", MCPConfig{TransportType: "sse"}, "sse"}, {"显式 http", MCPConfig{TransportType: "http"}, "http"}, {"streamable 归一为 http", MCPConfig{TransportType: "streamable"}, "http"}, {"留空 + 命令 ⇒ stdio", MCPConfig{Command: "npx"}, "stdio"}, {"留空 + 地址 ⇒ sse", MCPConfig{URL: "https://example.test/sse"}, "sse"}, {"命令优先于地址(与 newTransport 同序)", MCPConfig{Command: "npx", URL: "https://example.test"}, "stdio"}, {"都没有 ⇒ 空串(不编一个)", MCPConfig{}, ""}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { if got := transportNameOf(tc.cfg); got != tc.want { t.Fatalf("transportNameOf(%+v) = %q, want %q", tc.cfg, got, tc.want) } }) } }