package registry import ( "context" "testing" "git.yeij.top/AskaEth/Cyrene_v2/pkg/plugins/sdk" ) // 本文件钉住「来源章」(DevDocs/audit/MCP工具按上游分组-2026-10-24.md §2 方案①): // 归属只能来自注册时的元数据,因此盖章必须发生在**注册汇点**上,且热替换不能把它抹掉。 // // 三条判据各自**能变红**: // - 去掉 RegisterTool 里的盖章 → 第一条红; // - 去掉 ReplaceTool 里的盖章(它逐字段重建条目)→ 第二条红; // - 给所有工具都编一个章(比如按名字猜)→ 第三条红。 // originTool 是自报来源的最小工具(宿主内置工具不实现 sdk.OriginAware)。 type originTool struct { name string origin sdk.Origin } func (t originTool) Name() string { return t.name } func (t originTool) Description() string { return "来自 MCP" } func (t originTool) Parameters() (map[string]any, error) { return map[string]any{"type": "object"}, nil } func (t originTool) Execute(_ context.Context, _ any) (any, error) { return nil, nil } func (t originTool) Origin() sdk.Origin { return t.origin } var _ sdk.OriginAware = originTool{} // TestRegisterToolStampsOrigin:注册汇点把工具自报的来源盖在能力条目上。 func TestRegisterToolStampsOrigin(t *testing.T) { r := New() want := sdk.Origin{Kind: sdk.OriginKindMCP, Server: "fs", Transport: "stdio"} if err := r.RegisterTool(originTool{name: "read_file_mcp", origin: want}); err != nil { t.Fatalf("RegisterTool: %v", err) } cap, err := r.Get("read_file_mcp") if err != nil { t.Fatalf("Get: %v", err) } if cap.Origin != want { t.Fatalf("条目上的来源章 = %+v, want %+v(RegisterTool 是唯一汇点,必须在这里盖章)", cap.Origin, want) } } // TestReplaceToolKeepsOrigin:热替换必须重新盖章 —— 它逐字段重建 Capability, // 漏一位就等于「面板保存一次,这把工具从它的 server 分节里掉进内置」。 func TestReplaceToolKeepsOrigin(t *testing.T) { r := New() origin := sdk.Origin{Kind: sdk.OriginKindMCP, Server: "fs", Transport: "http"} if err := r.RegisterTool(originTool{name: "read_file_mcp", origin: origin}); err != nil { t.Fatalf("RegisterTool: %v", err) } // 热替换成同名的新实现,来源相同(换实现不换 server)。 if err := r.ReplaceTool(originTool{name: "read_file_mcp", origin: origin}); err != nil { t.Fatalf("ReplaceTool: %v", err) } cap, err := r.Get("read_file_mcp") if err != nil { t.Fatalf("Get: %v", err) } if cap.Origin != origin { t.Fatalf("热替换后来源章 = %+v, want %+v(ReplaceTool 重建条目,必须重新盖章)", cap.Origin, origin) } } // TestRegisterToolLeavesBuiltinOriginZero:宿主内置工具(没实现 OriginAware) // 的条目**没有**来源章。它是「键缺席」的根据 —— 读面据此省略 origin,而不是编一个。 func TestRegisterToolLeavesBuiltinOriginZero(t *testing.T) { r := New() if err := r.RegisterTool(&stubTool{name: "web_search", desc: "内置"}); err != nil { t.Fatalf("RegisterTool: %v", err) } cap, err := r.Get("web_search") if err != nil { t.Fatalf("Get: %v", err) } if !cap.Origin.IsZero() { t.Fatalf("内置工具却带来源章 %+v:没有 OriginAware 就不得编一个来源", cap.Origin) } } // TestSetPolicyKeepsOrigin:策略写路径不受来源章影响,也不许把它覆盖掉。 // 这是「tool_policies / 权限闸零影响」的断言 —— 策略仍按**名字**存, // 来源章只在条目上,两者互不干涉。 func TestSetPolicyKeepsOrigin(t *testing.T) { r := New() origin := sdk.Origin{Kind: sdk.OriginKindMCP, Server: "fs", Transport: "stdio"} if err := r.RegisterTool(originTool{name: "read_file_mcp", origin: origin}); err != nil { t.Fatalf("RegisterTool: %v", err) } if err := r.SetAllowInGroup("read_file_mcp", true); err != nil { t.Fatalf("SetAllowInGroup: %v", err) } cap, err := r.Get("read_file_mcp") if err != nil { t.Fatalf("Get: %v", err) } if !cap.Policy.AllowInGroup || cap.Policy.AllowInStranger { t.Fatalf("策略 = %+v, want 只有 AllowInGroup=true(按名字写的策略行)", cap.Policy) } if cap.Origin != origin { t.Fatalf("改策略把来源章冲掉了:%+v, want %+v", cap.Origin, origin) } }