Files
dingtalk-workspace-cli/internal/cli/schema_interface_registry_test.go
玉澜 7afd4139fc refactor(cli): drop dead discovery loader params and retired MCP pin machinery
- 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).
2026-08-03 21:15:20 +08:00

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()})
}