- NewSchemaCommand / NewMCPCommand / newCatalogCommand no longer accept a DiscoveryCatalogLoader they always discarded; schema's no-discovery property is now structural, retiring the panic-loader test guard along with the trivial root.go wrappers and the unused buildMCPCommandFn seam. - Delete the lazy sync.Once / atomic counter around the retired MCP pin: runtimeMCPMetadata only existed so a diagnostic counter could observe a loader that always returns the constant empty pin. Assembly now calls emptyPinnedMCPMetadata directly and SchemaMetadataLoadCounts loses the dead MCPMetadata field (the policy bans keep the retired names from reappearing).
280 lines
12 KiB
Go
280 lines
12 KiB
Go
// Copyright 2026 Alibaba Group
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package cli
|
|
|
|
import (
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
|
|
)
|
|
|
|
func TestBuildInterfaceRegistryIndexesCanonicalAndSharedRef(t *testing.T) {
|
|
shared := &embeddedMCPInterfaceRef{ProductID: "calendar-rpc", RPCName: "get_event"}
|
|
registry, err := buildInterfaceRegistry(map[string]embeddedMCPToolMetadata{
|
|
"calendar.event_get": {InterfaceRef: shared},
|
|
"calendar.event_read": {InterfaceRef: shared},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("buildInterfaceRegistry() error = %v", err)
|
|
}
|
|
if got := registry.ByCanonical["calendar.event_get"].Ref; got.ProductID != "calendar-rpc" || got.RPCName != "get_event" {
|
|
t.Fatalf("ByCanonical ref = %#v", got)
|
|
}
|
|
key := InterfaceRefKey{ProductID: "calendar-rpc", RPCName: "get_event"}
|
|
if got, want := registry.ByInterfaceRef[key], []string{"calendar.event_get", "calendar.event_read"}; !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("ByInterfaceRef = %#v, want %#v", got, want)
|
|
}
|
|
}
|
|
|
|
func TestBuildInterfaceRegistryRejectsCanonicalConflict(t *testing.T) {
|
|
ref := &embeddedMCPInterfaceRef{ProductID: "calendar", RPCName: "get_event"}
|
|
_, err := buildInterfaceRegistry(map[string]embeddedMCPToolMetadata{
|
|
"calendar.event_get": {InterfaceRef: ref},
|
|
" calendar.event_get ": {InterfaceRef: ref},
|
|
})
|
|
if err == nil || !strings.Contains(err.Error(), "conflicts") {
|
|
t.Fatalf("buildInterfaceRegistry() error = %v, want canonical conflict", err)
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageRuntimeEmbeddedMCPMetadataBuildsInterfaceRegistry(t *testing.T) {
|
|
// Production pin is empty; empty Tools must build a valid empty registry.
|
|
if _, err := buildInterfaceRegistry(emptyPinnedMCPMetadata().Tools); err != nil {
|
|
t.Fatalf("buildInterfaceRegistry(empty production MCP) error = %v", err)
|
|
}
|
|
if err := validateSchemaRegistryInterfaces(SchemaRegistry{Products: []ProductSpec{{
|
|
Tools: []ToolSpec{{
|
|
Identity: contract.ToolIdentitySpec{CanonicalPath: "doc.create", ProductID: "doc", Name: "create"},
|
|
Interface: contract.InterfaceSpec{Mode: contract.InterfaceModeLocal, Availability: contract.InterfaceAvailable},
|
|
}},
|
|
}}}); err != nil {
|
|
t.Fatalf("validateSchemaRegistryInterfaces(empty pin) error = %v", err)
|
|
}
|
|
if err := validateSchemaRegistryInterfacesWithMetadata(SchemaRegistry{Products: []ProductSpec{{
|
|
Tools: []ToolSpec{{
|
|
Identity: contract.ToolIdentitySpec{CanonicalPath: "doc.copy", ProductID: "doc", Name: "copy"},
|
|
Interface: contract.InterfaceSpec{
|
|
Mode: contract.InterfaceModeMCP,
|
|
Availability: contract.InterfaceAvailable,
|
|
Ref: &contract.InterfaceRefSpec{ProductID: "doc", RPCName: "copy_document"},
|
|
},
|
|
}},
|
|
}}}, embeddedMCPMetadata{Tools: map[string]embeddedMCPToolMetadata{
|
|
"doc.copy_document": {InterfaceRef: &embeddedMCPInterfaceRef{ProductID: "doc", RPCName: "copy_document"}},
|
|
}}); err != nil {
|
|
t.Fatalf("validateSchemaRegistryInterfacesWithMetadata(fixture) error = %v", err)
|
|
}
|
|
if err := validateSchemaRegistryInterfacesWithMetadata(SchemaRegistry{}, embeddedMCPMetadata{
|
|
Tools: map[string]embeddedMCPToolMetadata{"": {}},
|
|
}); err == nil {
|
|
t.Fatal("malformed fixture registry must fail closed")
|
|
}
|
|
}
|
|
|
|
func TestValidateSchemaRegistryInterfacesUsesExplicitRefNotCanonical(t *testing.T) {
|
|
metadata := embeddedMCPMetadata{Tools: map[string]embeddedMCPToolMetadata{
|
|
"calendar.snapshot_name": {
|
|
InterfaceRef: &embeddedMCPInterfaceRef{ProductID: "calendar-rpc", RPCName: "get_event"},
|
|
},
|
|
}}
|
|
|
|
err := validateSchemaRegistryInterfacesWithMetadata(schemaWithInterfaceTool(
|
|
"calendar.command_name_differs",
|
|
contract.InterfaceSpec{
|
|
Mode: "mcp",
|
|
Availability: "available",
|
|
Ref: &contract.InterfaceRefSpec{ProductID: "calendar-rpc", RPCName: "get_event"},
|
|
},
|
|
), metadata)
|
|
if err != nil {
|
|
t.Fatalf("validateSchemaRegistryInterfaces() error = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestValidateSchemaRegistryInterfacesRejectsMissingMCPRef(t *testing.T) {
|
|
err := validateSchemaRegistryInterfacesForTest(schemaWithInterfaceTool(
|
|
"calendar.event_get",
|
|
contract.InterfaceSpec{Mode: "mcp", Availability: "available"},
|
|
))
|
|
if err == nil || !strings.Contains(err.Error(), "has no interface_ref") {
|
|
t.Fatalf("validateSchemaRegistryInterfaces() error = %v, want missing ref", err)
|
|
}
|
|
}
|
|
|
|
func TestValidateSchemaRegistryInterfacesRejectsIncompleteMCPRef(t *testing.T) {
|
|
err := validateSchemaRegistryInterfacesForTest(schemaWithInterfaceTool(
|
|
"calendar.event_get",
|
|
contract.InterfaceSpec{
|
|
Mode: "mcp",
|
|
Availability: "available",
|
|
Ref: &contract.InterfaceRefSpec{ProductID: "calendar"},
|
|
},
|
|
))
|
|
if err == nil || !strings.Contains(err.Error(), "has incomplete interface_ref") {
|
|
t.Fatalf("validateSchemaRegistryInterfaces() error = %v, want incomplete ref", err)
|
|
}
|
|
}
|
|
|
|
func TestValidateSchemaRegistryInterfacesRejectsPhantomRef(t *testing.T) {
|
|
err := validateSchemaRegistryInterfacesForTest(schemaWithInterfaceTool(
|
|
"calendar.event_get",
|
|
contract.InterfaceSpec{
|
|
Mode: "mcp",
|
|
Availability: "available",
|
|
Ref: &contract.InterfaceRefSpec{ProductID: "calendar", RPCName: "phantom"},
|
|
},
|
|
))
|
|
if err == nil || !strings.Contains(err.Error(), "unknown MCP interface calendar.phantom") {
|
|
t.Fatalf("validateSchemaRegistryInterfaces() error = %v, want phantom ref", err)
|
|
}
|
|
}
|
|
|
|
func TestValidateSchemaRegistryInterfacesAllowsLocalWithoutRef(t *testing.T) {
|
|
err := validateSchemaRegistryInterfacesForTest(schemaWithInterfaceTool(
|
|
"calendar.local_cache",
|
|
contract.InterfaceSpec{Mode: "local", Availability: "available", Reason: "implemented by the local CLI"},
|
|
))
|
|
if err != nil {
|
|
t.Fatalf("validateSchemaRegistryInterfaces() error = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestValidateSchemaRegistryInterfacesAllowsCompositeWithoutRef(t *testing.T) {
|
|
err := validateSchemaRegistryInterfacesForTest(schemaWithInterfaceTool(
|
|
"calendar.composite_sync",
|
|
contract.InterfaceSpec{Mode: "composite", Availability: "available", Reason: "orchestrates multiple operations"},
|
|
))
|
|
if err != nil {
|
|
t.Fatalf("validateSchemaRegistryInterfaces() error = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestValidateSchemaRegistryInterfacesRejectsAnyLocalRef(t *testing.T) {
|
|
err := validateSchemaRegistryInterfacesForTest(schemaWithInterfaceTool(
|
|
"calendar.local_cache",
|
|
contract.InterfaceSpec{
|
|
Mode: "local",
|
|
Availability: "available",
|
|
Reason: "implemented by the local CLI",
|
|
Ref: &contract.InterfaceRefSpec{ProductID: "calendar", RPCName: "get_event"},
|
|
},
|
|
))
|
|
if err == nil || !strings.Contains(err.Error(), "must not declare interface_ref") {
|
|
t.Fatalf("validateSchemaRegistryInterfaces() error = %v, want forbidden local ref", err)
|
|
}
|
|
}
|
|
|
|
func TestValidateSchemaRegistryInterfacesAcceptsReviewedUnavailable(t *testing.T) {
|
|
err := validateSchemaRegistryInterfacesForTest(schemaWithInterfaceTool(
|
|
"calendar.retired",
|
|
contract.InterfaceSpec{Mode: "local", Availability: "unavailable", Reason: "the upstream operation was retired"},
|
|
))
|
|
if err != nil {
|
|
t.Fatalf("validateSchemaRegistryInterfaces() error = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestInterfaceSpecDispositionMatrix(t *testing.T) {
|
|
validRef := &contract.InterfaceRefSpec{ProductID: "calendar", RPCName: "get_event"}
|
|
for _, test := range []struct {
|
|
name string
|
|
spec contract.InterfaceSpec
|
|
wantError string
|
|
executable bool
|
|
}{
|
|
{name: "mcp available", spec: contract.InterfaceSpec{Mode: "mcp", Availability: "available", Ref: validRef}, executable: true},
|
|
{name: "local available", spec: contract.InterfaceSpec{Mode: "local", Availability: "available"}, executable: true},
|
|
{name: "composite available", spec: contract.InterfaceSpec{Mode: "composite", Availability: "available", Reason: "orchestrates operations"}, executable: true},
|
|
{name: "mcp unavailable", spec: contract.InterfaceSpec{Mode: "mcp", Availability: "unavailable", Reason: "RPC retired"}},
|
|
{name: "local unavailable", spec: contract.InterfaceSpec{Mode: "local", Availability: "unavailable", Reason: "compatibility command frozen"}},
|
|
{name: "composite unavailable", spec: contract.InterfaceSpec{Mode: "composite", Availability: "unavailable", Reason: "workflow retired"}},
|
|
{name: "unavailable is not a mode", spec: contract.InterfaceSpec{Mode: "unavailable", Availability: "unavailable", Reason: "retired"}, wantError: "legacy interface_mode=unavailable; migrate"},
|
|
{name: "missing mode", spec: contract.InterfaceSpec{Availability: "available"}, wantError: "has no interface mode"},
|
|
{name: "missing availability", spec: contract.InterfaceSpec{Mode: "local"}, wantError: "has no interface availability"},
|
|
{name: "unknown availability", spec: contract.InterfaceSpec{Mode: "local", Availability: "disabled"}, wantError: `unknown interface availability "disabled"`},
|
|
{name: "unavailable with ref", spec: contract.InterfaceSpec{Mode: "mcp", Availability: "unavailable", Reason: "retired", Ref: validRef}, wantError: "unavailable interface must not declare interface_ref"},
|
|
{name: "unavailable without reason", spec: contract.InterfaceSpec{Mode: "local", Availability: "unavailable"}, wantError: "must declare interface_reason"},
|
|
{name: "available mcp without ref", spec: contract.InterfaceSpec{Mode: "mcp", Availability: "available"}, wantError: "has no interface_ref"},
|
|
{name: "available local with ref", spec: contract.InterfaceSpec{Mode: "local", Availability: "available", Ref: validRef}, wantError: "must not declare interface_ref"},
|
|
{name: "available composite with ref", spec: contract.InterfaceSpec{Mode: "composite", Availability: "available", Reason: "orchestrates operations", Ref: validRef}, wantError: "must not declare a single interface_ref"},
|
|
{name: "available composite without reason", spec: contract.InterfaceSpec{Mode: "composite", Availability: "available"}, wantError: "must declare interface_reason"},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
err := validateSchemaRegistryInterfacesForTest(schemaWithInterfaceTool("calendar.event_get", test.spec))
|
|
if test.wantError == "" {
|
|
if err != nil {
|
|
t.Fatalf("validateSchemaRegistryInterfaces() error = %v", err)
|
|
}
|
|
} else if err == nil || !strings.Contains(err.Error(), test.wantError) {
|
|
t.Fatalf("validateSchemaRegistryInterfaces() error = %v, want %q", err, test.wantError)
|
|
}
|
|
if got := test.spec.AgentExecutable(); got != test.executable {
|
|
t.Fatalf("AgentExecutable() = %v, want %v", got, test.executable)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateSchemaRegistryInterfacesRejectsUnknownAndMissingMode(t *testing.T) {
|
|
t.Run("missing", func(t *testing.T) {
|
|
err := validateSchemaRegistryInterfacesForTest(schemaWithInterfaceTool(
|
|
"calendar.event_get",
|
|
contract.InterfaceSpec{Availability: "available"},
|
|
))
|
|
if err == nil || !strings.Contains(err.Error(), "has no interface mode") {
|
|
t.Fatalf("validateSchemaRegistryInterfaces() error = %v, want missing mode", err)
|
|
}
|
|
})
|
|
|
|
t.Run("unknown", func(t *testing.T) {
|
|
err := validateSchemaRegistryInterfacesForTest(schemaWithInterfaceTool(
|
|
"calendar.event_get",
|
|
contract.InterfaceSpec{Mode: "remote", Availability: "available"},
|
|
))
|
|
if err == nil || !strings.Contains(err.Error(), `unknown interface mode "remote"`) {
|
|
t.Fatalf("validateSchemaRegistryInterfaces() error = %v, want unknown mode", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func schemaWithInterfaceTool(canonical string, interfaceSpec contract.InterfaceSpec) SchemaRegistry {
|
|
parts := strings.SplitN(canonical, ".", 2)
|
|
identity := contract.ToolIdentitySpec{CanonicalPath: canonical}
|
|
if len(parts) == 2 {
|
|
identity.ProductID = parts[0]
|
|
identity.Name = parts[1]
|
|
}
|
|
return SchemaRegistry{Products: []ProductSpec{{
|
|
ID: identity.ProductID,
|
|
Tools: []ToolSpec{{
|
|
Identity: identity,
|
|
Interface: interfaceSpec,
|
|
}},
|
|
}}}
|
|
}
|
|
|
|
func validInterfaceTools() map[string]embeddedMCPToolMetadata {
|
|
return map[string]embeddedMCPToolMetadata{
|
|
"calendar.event_get": {
|
|
InterfaceRef: &embeddedMCPInterfaceRef{ProductID: "calendar", RPCName: "get_event"},
|
|
},
|
|
}
|
|
}
|
|
|
|
func validateSchemaRegistryInterfacesForTest(schema SchemaRegistry) error {
|
|
return validateSchemaRegistryInterfacesWithMetadata(schema, embeddedMCPMetadata{Tools: validInterfaceTools()})
|
|
}
|