cli/internal/bnp/mcp_tools_test.go
2026-08-10 22:49:09 +08:00

111 lines
3.3 KiB
Go

package bnp
import (
"path/filepath"
"strings"
"testing"
abiv1 "git.dev.alexdunmow.com/block/pluginsdk/abi/v1"
"google.golang.org/protobuf/proto"
)
func wikiMCPManifest(hasHTTPHandler bool) *abiv1.PluginManifest {
procedure := "/wiki.v1.WikiService/SaveArticle"
return &abiv1.PluginManifest{
AbiVersion: 1,
Name: "wiki",
Version: "0.1.12",
HasHttpHandler: hasHTTPHandler,
RbacMethodRoles: map[string]string{procedure: "admin"},
McpTools: []*abiv1.MCPToolDescriptor{{
Procedure: procedure,
ServiceFullName: "wiki.v1.WikiService",
MethodName: "SaveArticle",
InputSchemaJson: []byte(`{"type":"object","properties":{"article":{"type":"object"}}}`),
}},
}
}
func TestValidateMCPManifestAcceptsCallableWikiDescriptor(t *testing.T) {
manifest := wikiMCPManifest(true)
if err := ValidateMCPManifest(manifest, "@ninja", "wiki"); err != nil {
t.Fatalf("ValidateMCPManifest() error = %v", err)
}
name, err := mcpToolName("@ninja", "wiki", "wiki.v1.WikiService", "SaveArticle")
if err != nil {
t.Fatalf("mcpToolName() error = %v", err)
}
if name != "plugin_ninja_wiki_v1_save_article" {
t.Fatalf("tool name = %q", name)
}
}
func TestValidateMCPManifestRejectsMissingRuntimeHandler(t *testing.T) {
err := ValidateMCPManifest(wikiMCPManifest(false), "@ninja", "wiki")
if err == nil || !strings.Contains(err.Error(), "has_http_handler") {
t.Fatalf("error = %v, want has_http_handler rejection", err)
}
}
func TestValidateMCPManifestRejectsMalformedDescriptors(t *testing.T) {
tests := []struct {
name string
mutate func(*abiv1.PluginManifest)
want string
}{
{
name: "unmapped procedure",
mutate: func(manifest *abiv1.PluginManifest) {
manifest.RbacMethodRoles = nil
},
want: "RBAC role",
},
{
name: "invalid schema",
mutate: func(manifest *abiv1.PluginManifest) {
manifest.McpTools[0].InputSchemaJson = []byte(`{"type":"string"}`)
},
want: "input schema",
},
{
name: "missing API major",
mutate: func(manifest *abiv1.PluginManifest) {
descriptor := manifest.McpTools[0]
descriptor.Procedure = "/wiki.WikiService/SaveArticle"
descriptor.ServiceFullName = "wiki.WikiService"
manifest.RbacMethodRoles = map[string]string{descriptor.Procedure: "admin"}
},
want: "API major",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
manifest := wikiMCPManifest(true)
test.mutate(manifest)
err := ValidateMCPManifest(manifest, "@ninja", "wiki")
if err == nil || !strings.Contains(err.Error(), test.want) {
t.Fatalf("error = %v, want %q", err, test.want)
}
})
}
}
func TestVerifyRejectsMCPDescriptorsWithoutRuntimeHandler(t *testing.T) {
manifestBytes, err := proto.Marshal(wikiMCPManifest(false))
if err != nil {
t.Fatalf("marshal manifest: %v", err)
}
out := filepath.Join(t.TempDir(), "wiki.bnp")
_, err = packArtifact(out, []packEntry{
{ArtifactPath: fileWasm, Data: []byte("wasm")},
{ArtifactPath: fileMod, Data: []byte("[plugin]\nname = \"wiki\"\nscope = \"@ninja\"\nversion = \"0.1.12\"\n")},
{ArtifactPath: fileManifest, Data: manifestBytes},
})
if err != nil {
t.Fatalf("pack artifact: %v", err)
}
if _, err := Verify(out); err == nil || !strings.Contains(err.Error(), "has_http_handler") {
t.Fatalf("Verify() error = %v, want has_http_handler rejection", err)
}
}