Files
dingtalk-workspace-cli/internal/compat/registry_test.go
T
xianfeng wang bbca507792 feat(discovery): MCP discovery service abstraction with positional params, examples, defaults, and schema v3 extensions (#156)
* discovery

* discovery

* wk discovery

* wk discovery

* wk discovery

* wk discovery

* wk discovery

* wk discovery

* discovery example and default

* position param

* Fix test

* dev auto login

* fix test

* fix conflict
2026-04-24 14:08:17 +08:00

313 lines
9.7 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 compat
import (
"reflect"
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cobracmd"
"github.com/spf13/cobra"
)
func TestApplyBindings_PositionalWithFlagAliases(t *testing.T) {
t.Parallel()
// envelope shape: { keyword: { alias: "query", aliases: ["keyword"], positional: true } }
bindings := []FlagBinding{
{
FlagName: "query",
Aliases: []string{"keyword"},
Property: "keyword",
Kind: ValueString,
Usage: "搜索关键词",
Required: true,
Positional: true,
PositionalIndex: 0,
},
}
cmd := &cobra.Command{Use: "search"}
ApplyBindings(cmd, bindings)
primary := cmd.Flags().Lookup("query")
if primary == nil {
t.Fatal("--query flag should be registered for dual-mode positional")
}
if primary.Hidden {
t.Fatal("--query flag should be visible")
}
hidden := cmd.Flags().Lookup("keyword")
if hidden == nil {
t.Fatal("--keyword alias flag should be registered")
}
if !hidden.Hidden {
t.Fatal("--keyword alias flag should be hidden")
}
// --query should NOT be marked required at cobra level — that would
// break flag-only invocation when arity is relaxed.
if _, ok := primary.Annotations[cobra.BashCompOneRequiredFlag]; ok {
t.Fatal("--query should not be MarkFlagRequired (validation happens in RunE)")
}
}
func TestApplyBindings_PurePositionalSkipsFlagRegistration(t *testing.T) {
t.Parallel()
// Pure positional (no Alias / no Aliases) → no flag should be registered;
// arity validator (set in NewDirectCommand) handles required-presence.
bindings := []FlagBinding{
{
Property: "text",
Kind: ValueString,
Required: true,
Positional: true,
PositionalIndex: 0,
},
}
cmd := &cobra.Command{Use: "send"}
ApplyBindings(cmd, bindings)
if f := cmd.Flags().Lookup("text"); f != nil {
t.Fatalf("pure positional should not register a flag, got %+v", f)
}
}
func TestCollectBindingsParsesTypedValuesAndAcceptsAliasFlags(t *testing.T) {
t.Parallel()
bindings := []FlagBinding{
{FlagName: "dept-ids", Alias: "deptIds", Property: "deptIds", Kind: ValueFloatSlice},
{FlagName: "ratio", Property: "ratio", Kind: ValueFloat},
{FlagName: "enabled-flags", Property: "enabledFlags", Kind: ValueBoolSlice},
{FlagName: "base-id", Alias: "baseId", Property: "baseId", Kind: ValueString},
}
cmd := &cobra.Command{Use: "test"}
ApplyBindings(cmd, bindings)
if err := cmd.Flags().Set("deptIds", "1,2.5"); err != nil {
t.Fatalf("Set(deptIds) error = %v", err)
}
if err := cmd.Flags().Set("ratio", "1.25"); err != nil {
t.Fatalf("Set(ratio) error = %v", err)
}
if err := cmd.Flags().Set("enabled-flags", "true,false"); err != nil {
t.Fatalf("Set(enabled-flags) error = %v", err)
}
if err := cmd.Flags().Set("baseId", "B1"); err != nil {
t.Fatalf("Set(baseId) error = %v", err)
}
params, err := CollectBindings(cmd, bindings, nil)
if err != nil {
t.Fatalf("CollectBindings() error = %v", err)
}
if params["baseId"] != "B1" {
t.Fatalf("baseId = %#v, want B1", params["baseId"])
}
if params["ratio"] != 1.25 {
t.Fatalf("ratio = %#v, want 1.25", params["ratio"])
}
if want := []any{1.0, 2.5}; !reflect.DeepEqual(params["deptIds"], want) {
t.Fatalf("deptIds = %#v, want %#v", params["deptIds"], want)
}
if want := []any{true, false}; !reflect.DeepEqual(params["enabledFlags"], want) {
t.Fatalf("enabledFlags = %#v, want %#v", params["enabledFlags"], want)
}
aliasFlag := cmd.Flags().Lookup("baseId")
if aliasFlag == nil || !aliasFlag.Hidden {
t.Fatalf("baseId alias flag hidden = false, want true")
}
}
func TestHiddenAliasFlagsDoNotInflateLeafCount(t *testing.T) {
t.Parallel()
// overlay leaf: 2 primary + 2 hidden aliases = 4 total, but only 2 visible
overlay := &cobra.Command{Use: "list"}
ApplyBindings(overlay, []FlagBinding{
{FlagName: "start-time", Alias: "startTime", Property: "startTime", Kind: ValueString},
{FlagName: "end-time", Alias: "endTime", Property: "endTime", Kind: ValueString},
})
// curated compat leaf: 3 primary, 0 aliases = 3 visible
curated := &cobra.Command{Use: "list"}
ApplyBindings(curated, []FlagBinding{
{FlagName: "start", Property: "start", Kind: ValueString},
{FlagName: "end", Property: "end", Kind: ValueString},
{FlagName: "calendar-id", Property: "calendarId", Kind: ValueString},
})
overlayCount := cobracmd.LocalFlagCount(overlay)
curatedCount := cobracmd.LocalFlagCount(curated)
// overlay has 2 visible flags (start-time, end-time) + json + params = 4
// curated has 3 visible flags (start, end, calendar-id) + json + params = 5
if overlayCount >= curatedCount {
t.Fatalf("overlay visible flags (%d) >= curated visible flags (%d); hidden aliases should not be counted",
overlayCount, curatedCount)
}
// shouldReplaceCompatLeaf should NOT replace curated with overlay
if cobracmd.ShouldReplaceLeaf(curated, overlay) {
t.Fatal("cobracmd.ShouldReplaceLeaf(curated, overlay) = true; overlay should not displace curated")
}
}
func TestCollectBindingsParsesJSONFlagValue(t *testing.T) {
t.Parallel()
bindings := []FlagBinding{
{FlagName: "fields", Property: "fields", Kind: ValueJSON},
{FlagName: "config", Property: "config", Kind: ValueJSON},
}
cmd := &cobra.Command{Use: "test"}
ApplyBindings(cmd, bindings)
if err := cmd.Flags().Set("fields", `[{"fieldName":"title","type":"text"}]`); err != nil {
t.Fatalf("Set(fields) error = %v", err)
}
if err := cmd.Flags().Set("config", `{"options":[{"name":"high"}]}`); err != nil {
t.Fatalf("Set(config) error = %v", err)
}
params, err := CollectBindings(cmd, bindings, nil)
if err != nil {
t.Fatalf("CollectBindings() error = %v", err)
}
fields, ok := params["fields"].([]any)
if !ok || len(fields) != 1 {
t.Fatalf("fields = %#v, want array of 1 element", params["fields"])
}
firstField, ok := fields[0].(map[string]any)
if !ok || firstField["fieldName"] != "title" {
t.Fatalf("fields[0] = %#v, want {fieldName:title, type:text}", fields[0])
}
config, ok := params["config"].(map[string]any)
if !ok {
t.Fatalf("config = %#v, want map", params["config"])
}
options, ok := config["options"].([]any)
if !ok || len(options) != 1 {
t.Fatalf("config.options = %#v, want array of 1", config["options"])
}
}
func TestCollectSchemaFlagsPicksUpUnboundFlags(t *testing.T) {
t.Parallel()
// Simulate a plugin command with schema-generated flags but no bindings.
cmd := &cobra.Command{Use: "greet"}
cmd.Flags().String("name", "", "Name of person")
cmd.Flags().String("language", "en", "Language")
cmd.Flags().Int("count", 0, "Repeat count")
cmd.Flags().Bool("loud", false, "Loud mode")
cmd.Flags().String("json", "", "")
cmd.Flags().String("params", "", "")
// User sets --name and --count but not --language
_ = cmd.Flags().Set("name", "Alice")
_ = cmd.Flags().Set("count", "3")
_ = cmd.Flags().Set("loud", "true")
params := make(map[string]any)
collectSchemaFlags(cmd, nil, params)
if params["name"] != "Alice" {
t.Errorf("name = %v, want Alice", params["name"])
}
if params["count"] != 3 {
t.Errorf("count = %v, want 3", params["count"])
}
if params["loud"] != true {
t.Errorf("loud = %v, want true", params["loud"])
}
// language was not set by user, should not appear
if _, exists := params["language"]; exists {
t.Errorf("language should not be in params (not set by user)")
}
// json/params are reserved, should not appear
if _, exists := params["json"]; exists {
t.Error("json should be skipped")
}
}
func TestCollectSchemaFlagsSkipsBoundFlags(t *testing.T) {
t.Parallel()
bindings := []FlagBinding{
{FlagName: "dept-id", Property: "deptId", Kind: ValueString},
}
cmd := &cobra.Command{Use: "test"}
ApplyBindings(cmd, bindings)
// Also add a schema-generated flag
cmd.Flags().String("title", "", "Title")
_ = cmd.Flags().Set("dept-id", "D001")
_ = cmd.Flags().Set("title", "Hello")
params := make(map[string]any)
collectSchemaFlags(cmd, bindings, params)
// dept-id is bound, should NOT be collected by collectSchemaFlags
if _, exists := params["dept_id"]; exists {
t.Error("dept-id should be skipped (already has binding)")
}
// title is unbound, should be collected
if params["title"] != "Hello" {
t.Errorf("title = %v, want Hello", params["title"])
}
}
func TestCollectSchemaFlagsSkipsGlobalFlags(t *testing.T) {
t.Parallel()
cmd := &cobra.Command{Use: "test"}
cmd.Flags().String("name", "", "Name")
cmd.Flags().Bool("debug", false, "Debug")
cmd.Flags().Bool("verbose", false, "Verbose")
cmd.Flags().Bool("dry-run", false, "Dry run")
cmd.Flags().String("format", "json", "Format")
cmd.Flags().String("json", "", "")
cmd.Flags().String("params", "", "")
_ = cmd.Flags().Set("name", "Bob")
_ = cmd.Flags().Set("debug", "true")
_ = cmd.Flags().Set("verbose", "true")
_ = cmd.Flags().Set("dry-run", "true")
_ = cmd.Flags().Set("format", "table")
params := make(map[string]any)
collectSchemaFlags(cmd, nil, params)
if params["name"] != "Bob" {
t.Errorf("name = %v, want Bob", params["name"])
}
// Global flags should be skipped
for _, skip := range []string{"debug", "verbose", "dry_run", "format"} {
if _, exists := params[skip]; exists {
t.Errorf("%s should be skipped (global flag)", skip)
}
}
}