- add 45 public document shortcuts and 2 reviewed expert-only paths - preserve six historical command and Schema identities alongside canonical leaves - add safe local download primitives and document access/share orchestration - keep comment create/reply confirmation backward-compatible - ensure grant-and-share upgrades insufficient roles before messaging - return non-zero partial/failure message ledgers and structured partial-write recovery metadata - enumerate every selection candidate and use rune-safe Unicode keyword contexts - assert zero-call confirmation boundaries for destructive shortcuts Validation: - full Go test suite and repository policy - real DingTalk E2E for 34 canonical shortcuts, all 8 compatibility-affected entries, READER-to-EDITOR grant-and-share upgrade, and same-block selection ambiguity with zero comment writes - command compatibility across 1,221 historical nodes and complete Schema compatibility - 1,152 Agent examples including 62 real Cobra dry-runs - 100% changed-code coverage across 1,260 executable statements
218 lines
7.1 KiB
Go
218 lines
7.1 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 builtin_test
|
||
|
||
import (
|
||
"strconv"
|
||
"strings"
|
||
"testing"
|
||
|
||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut"
|
||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut/builtin"
|
||
"github.com/spf13/cobra"
|
||
"github.com/spf13/pflag"
|
||
)
|
||
|
||
// TestCmdcoreMountPreservesEveryBuiltInShortcutSurface is the differential
|
||
// guard for the live mount migration. It derives the historical Cobra surface
|
||
// directly from each Shortcut declaration and checks the command-built tree.
|
||
func TestCrossPlatformCoverageCmdcoreMountPreservesEveryBuiltInShortcutSurface(t *testing.T) {
|
||
mounted := map[string]*cobra.Command{}
|
||
for _, service := range builtin.BaseCommands() {
|
||
for _, command := range service.Commands() {
|
||
mounted[service.Name()+"\x00"+command.Name()] = command
|
||
}
|
||
}
|
||
|
||
var declarations int
|
||
for _, spec := range shortcut.All() {
|
||
if spec.UserDefined {
|
||
continue
|
||
}
|
||
declarations++
|
||
key := spec.Service + "\x00" + spec.Command
|
||
command := mounted[key]
|
||
if command == nil {
|
||
t.Errorf("%s %s: command is not mounted", spec.Service, spec.Command)
|
||
continue
|
||
}
|
||
if command.Short != spec.Description {
|
||
t.Errorf("%s %s: Short = %q, want %q", spec.Service, spec.Command, command.Short, spec.Description)
|
||
}
|
||
if command.Long != expectedShortcutLong(spec) {
|
||
t.Errorf("%s %s: Long mismatch\n got: %q\nwant: %q",
|
||
spec.Service, spec.Command, command.Long, expectedShortcutLong(spec))
|
||
}
|
||
if command.Example != expectedShortcutExamples(spec.Tips) {
|
||
t.Errorf("%s %s: Example = %q, want %q",
|
||
spec.Service, spec.Command, command.Example, expectedShortcutExamples(spec.Tips))
|
||
}
|
||
if command.Hidden != spec.Hidden {
|
||
t.Errorf("%s %s: Hidden = %v, want %v", spec.Service, spec.Command, command.Hidden, spec.Hidden)
|
||
}
|
||
|
||
declaredFlags := make(map[string]shortcut.Flag, len(spec.Flags))
|
||
for _, flag := range spec.Flags {
|
||
declaredFlags[flag.Name] = flag
|
||
got := command.Flags().Lookup(flag.Name)
|
||
if got == nil {
|
||
t.Errorf("%s %s: flag --%s is not mounted", spec.Service, spec.Command, flag.Name)
|
||
continue
|
||
}
|
||
if got.Value.Type() != expectedFlagType(flag.Type) {
|
||
t.Errorf("%s %s --%s: type = %q, want %q",
|
||
spec.Service, spec.Command, flag.Name, got.Value.Type(), expectedFlagType(flag.Type))
|
||
}
|
||
if got.Usage != expectedFlagHelp(flag) {
|
||
t.Errorf("%s %s --%s: usage = %q, want %q",
|
||
spec.Service, spec.Command, flag.Name, got.Usage, expectedFlagHelp(flag))
|
||
}
|
||
if got.Hidden != flag.Hidden {
|
||
t.Errorf("%s %s --%s: hidden = %v, want %v",
|
||
spec.Service, spec.Command, flag.Name, got.Hidden, flag.Hidden)
|
||
}
|
||
assertShortcutDefault(t, command, spec, flag)
|
||
for _, alias := range flag.Aliases {
|
||
declaredFlags[alias] = flag
|
||
gotAlias := command.Flags().Lookup(alias)
|
||
if gotAlias == nil {
|
||
t.Errorf("%s %s: flag alias --%s is not mounted", spec.Service, spec.Command, alias)
|
||
continue
|
||
}
|
||
wantHidden := !flag.AliasesVisible
|
||
if gotAlias.Hidden != wantHidden {
|
||
t.Errorf("%s %s: flag alias --%s hidden = %v, want %v", spec.Service, spec.Command, alias, gotAlias.Hidden, wantHidden)
|
||
}
|
||
}
|
||
}
|
||
command.Flags().VisitAll(func(flag *pflag.Flag) {
|
||
if flag.Name == "help" {
|
||
return
|
||
}
|
||
if _, ok := declaredFlags[flag.Name]; !ok {
|
||
t.Errorf("%s %s: undeclared flag --%s was mounted", spec.Service, spec.Command, flag.Name)
|
||
}
|
||
})
|
||
}
|
||
if len(mounted) != declarations {
|
||
t.Fatalf("mounted built-in shortcuts = %d, declarations = %d", len(mounted), declarations)
|
||
}
|
||
}
|
||
|
||
func expectedShortcutLong(spec shortcut.Shortcut) string {
|
||
long := strings.TrimSpace(spec.Intent)
|
||
if long == "" {
|
||
long = strings.TrimSpace(spec.Description)
|
||
}
|
||
if len(spec.Constraints) == 0 {
|
||
return long
|
||
}
|
||
lines := make([]string, 0, len(spec.Constraints))
|
||
for _, constraint := range spec.Constraints {
|
||
text := strings.TrimSpace(constraint.Description)
|
||
if text == "" {
|
||
switch constraint.Kind {
|
||
case shortcut.ConstraintAtLeastOne:
|
||
text = expectedDashed(constraint.Flags) + " 至少指定一个"
|
||
case shortcut.ConstraintExactlyOne:
|
||
text = expectedDashed(constraint.Flags) + " 必须且只能指定一个"
|
||
case shortcut.ConstraintMutuallyExclusive:
|
||
text = expectedDashed(constraint.Flags) + " 互斥,最多指定一个"
|
||
}
|
||
}
|
||
lines = append(lines, " - "+text)
|
||
}
|
||
return long + "\n\n参数约束:\n" + strings.Join(lines, "\n")
|
||
}
|
||
|
||
func expectedShortcutExamples(tips []string) string {
|
||
if len(tips) == 0 {
|
||
return ""
|
||
}
|
||
return " " + strings.Join(tips, "\n ")
|
||
}
|
||
|
||
func expectedFlagType(kind shortcut.FlagType) string {
|
||
switch kind {
|
||
case shortcut.FlagBool:
|
||
return "bool"
|
||
case shortcut.FlagInt:
|
||
return "int"
|
||
case shortcut.FlagStringSlice:
|
||
return "stringSlice"
|
||
default:
|
||
return "string"
|
||
}
|
||
}
|
||
|
||
func expectedFlagHelp(flag shortcut.Flag) string {
|
||
parts := make([]string, 0, 2)
|
||
if flag.Required && !strings.Contains(flag.Desc, "必填") {
|
||
parts = append(parts, "必填")
|
||
}
|
||
if len(flag.Enum) > 0 {
|
||
parts = append(parts, "可选值: "+strings.Join(flag.Enum, ", "))
|
||
}
|
||
if len(parts) == 0 {
|
||
return flag.Desc
|
||
}
|
||
if strings.TrimSpace(flag.Desc) == "" {
|
||
return strings.Join(parts, ";")
|
||
}
|
||
return flag.Desc + "(" + strings.Join(parts, ";") + ")"
|
||
}
|
||
|
||
func assertShortcutDefault(t *testing.T, command *cobra.Command, spec shortcut.Shortcut, flag shortcut.Flag) {
|
||
t.Helper()
|
||
switch flag.Type {
|
||
case shortcut.FlagBool:
|
||
got, err := command.Flags().GetBool(flag.Name)
|
||
if err != nil || got != (flag.Default == "true") {
|
||
t.Errorf("%s %s --%s: bool default = %v/%v, want %v",
|
||
spec.Service, spec.Command, flag.Name, got, err, flag.Default == "true")
|
||
}
|
||
case shortcut.FlagInt:
|
||
want, _ := strconv.Atoi(flag.Default)
|
||
got, err := command.Flags().GetInt(flag.Name)
|
||
if err != nil || got != want {
|
||
t.Errorf("%s %s --%s: int default = %d/%v, want %d",
|
||
spec.Service, spec.Command, flag.Name, got, err, want)
|
||
}
|
||
case shortcut.FlagStringSlice:
|
||
var want []string
|
||
if value := strings.TrimSpace(flag.Default); value != "" {
|
||
want = strings.Split(value, ",")
|
||
}
|
||
got, err := command.Flags().GetStringSlice(flag.Name)
|
||
if err != nil || strings.Join(got, "\x00") != strings.Join(want, "\x00") {
|
||
t.Errorf("%s %s --%s: string-slice default = %#v/%v, want %#v",
|
||
spec.Service, spec.Command, flag.Name, got, err, want)
|
||
}
|
||
default:
|
||
got, err := command.Flags().GetString(flag.Name)
|
||
if err != nil || got != flag.Default {
|
||
t.Errorf("%s %s --%s: string default = %q/%v, want %q",
|
||
spec.Service, spec.Command, flag.Name, got, err, flag.Default)
|
||
}
|
||
}
|
||
}
|
||
|
||
func expectedDashed(flags []string) string {
|
||
out := make([]string, len(flags))
|
||
for i, flag := range flags {
|
||
out[i] = "--" + flag
|
||
}
|
||
return strings.Join(out, "、")
|
||
}
|