pluginsdk/plugin/wasmguest/mcp_test.go
2026-09-05 21:14:15 +08:00

101 lines
4.3 KiB
Go

package wasmguest
import (
"encoding/json"
"testing"
"git.dev.alexdunmow.com/block/pluginsdk/rbac"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protodesc"
"google.golang.org/protobuf/reflect/protoregistry"
"google.golang.org/protobuf/types/descriptorpb"
_ "google.golang.org/protobuf/types/known/timestamppb"
)
func TestCaptureMCPToolsUsesCompiledProtobufDescriptor(t *testing.T) {
registerMCPFixture(t)
procedure := "/mcpfixture.v1.WikiService/ListArticles"
tools := captureMCPTools(map[string]rbac.Role{procedure: rbac.RoleAdmin})
if len(tools) != 1 {
t.Fatalf("captureMCPTools() returned %d tools, want 1", len(tools))
}
tool := tools[0]
if tool.GetProcedure() != procedure || tool.GetServiceFullName() != "mcpfixture.v1.WikiService" || tool.GetMethodName() != "ListArticles" {
t.Fatalf("unexpected procedure metadata: %#v", tool)
}
if tool.GetDescription() != "Lists Wiki articles." || tool.GetDocumentation() != "ListArticles lists Wiki articles.\n\nSupports pagination." {
t.Fatalf("unexpected descriptor documentation: description=%q doc=%q", tool.GetDescription(), tool.GetDocumentation())
}
var schema map[string]any
if err := json.Unmarshal(tool.GetInputSchemaJson(), &schema); err != nil {
t.Fatalf("schema is not JSON: %v", err)
}
properties, ok := schema["properties"].(map[string]any)
if !ok {
t.Fatalf("schema properties = %#v", schema["properties"])
}
assertSchemaType(t, properties, "search", "string")
assertSchemaType(t, properties, "page", "integer")
assertSchemaType(t, properties, "published_after", "string")
}
func TestCaptureMCPToolsSkipsUnresolvableAndStreamingProcedures(t *testing.T) {
registerMCPFixture(t)
tools := captureMCPTools(map[string]rbac.Role{
"/mcpfixture.v1.WikiService/StreamArticles": rbac.RoleAdmin,
"/missing.v1.WikiService/ListArticles": rbac.RoleAdmin,
"not-a-procedure": rbac.RoleAdmin,
})
if len(tools) != 0 {
t.Fatalf("captureMCPTools() returned %d tools, want fail-closed omission", len(tools))
}
}
func assertSchemaType(t *testing.T, properties map[string]any, field, expected string) {
t.Helper()
property, ok := properties[field].(map[string]any)
if !ok || property["type"] != expected {
t.Fatalf("schema field %q = %#v, want type %q", field, properties[field], expected)
}
}
func registerMCPFixture(t *testing.T) {
t.Helper()
if _, err := protoregistry.GlobalFiles.FindFileByPath("test/mcpfixture.proto"); err == nil {
return
}
file, err := protodesc.NewFile(&descriptorpb.FileDescriptorProto{
Name: new("test/mcpfixture.proto"),
Package: new("mcpfixture.v1"),
Syntax: new("proto3"),
Dependency: []string{"google/protobuf/timestamp.proto"},
MessageType: []*descriptorpb.DescriptorProto{{
Name: new("ListArticlesRequest"),
Field: []*descriptorpb.FieldDescriptorProto{
{Name: new("search"), Number: proto.Int32(1), Label: descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum(), Type: descriptorpb.FieldDescriptorProto_TYPE_STRING.Enum()},
{Name: new("page"), Number: proto.Int32(2), Label: descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum(), Type: descriptorpb.FieldDescriptorProto_TYPE_INT32.Enum()},
{Name: new("published_after"), Number: proto.Int32(3), Label: descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum(), Type: descriptorpb.FieldDescriptorProto_TYPE_MESSAGE.Enum(), TypeName: new(".google.protobuf.Timestamp")},
},
}, {
Name: new("ListArticlesResponse"),
}},
Service: []*descriptorpb.ServiceDescriptorProto{{
Name: new("WikiService"),
Method: []*descriptorpb.MethodDescriptorProto{{
Name: new("ListArticles"), InputType: new(".mcpfixture.v1.ListArticlesRequest"), OutputType: new(".mcpfixture.v1.ListArticlesResponse"),
}, {
Name: new("StreamArticles"), InputType: new(".mcpfixture.v1.ListArticlesRequest"), OutputType: new(".mcpfixture.v1.ListArticlesResponse"), ServerStreaming: new(true),
}},
}},
SourceCodeInfo: &descriptorpb.SourceCodeInfo{Location: []*descriptorpb.SourceCodeInfo_Location{{
Path: []int32{6, 0, 2, 0}, Span: []int32{1, 0, 1, 1}, LeadingComments: new("ListArticles lists Wiki articles.\n\nSupports pagination."),
}}},
}, protoregistry.GlobalFiles)
if err != nil {
t.Fatalf("create fixture descriptor: %v", err)
}
if err := protoregistry.GlobalFiles.RegisterFile(file); err != nil {
t.Fatalf("register fixture descriptor: %v", err)
}
}