229 lines
8.5 KiB
Go
229 lines
8.5 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 app
|
|
|
|
import (
|
|
stderrors "errors"
|
|
"reflect"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/pipeline"
|
|
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/pipeline/handlers"
|
|
"github.com/spf13/cobra"
|
|
"github.com/spf13/pflag"
|
|
)
|
|
|
|
func TestAllDistributionBooleanFlagTypesNormalizeDetachedLiterals(t *testing.T) {
|
|
root := NewSchemaSourceRootCommand()
|
|
unique := make(map[string]pipeline.FlagInfo)
|
|
var visit func(*cobra.Command)
|
|
visit = func(command *cobra.Command) {
|
|
for _, spec := range pipeline.FlagInfoFromCommand(command) {
|
|
if spec.Type != "bool" && spec.Type != "boolean" {
|
|
continue
|
|
}
|
|
key := strings.Join([]string{spec.Name, spec.Shorthand, spec.Type}, "\x00")
|
|
unique[key] = spec
|
|
}
|
|
for _, child := range command.Commands() {
|
|
visit(child)
|
|
}
|
|
}
|
|
visit(root)
|
|
|
|
keys := make([]string, 0, len(unique))
|
|
for key := range unique {
|
|
keys = append(keys, key)
|
|
}
|
|
sort.Strings(keys)
|
|
if len(keys) < 80 {
|
|
t.Fatalf("boolean flag contract coverage is unexpectedly small: %d", len(keys))
|
|
}
|
|
|
|
for _, key := range keys {
|
|
spec := unique[key]
|
|
for _, value := range []string{"true", "false"} {
|
|
t.Run(spec.Name+"/"+value, func(t *testing.T) {
|
|
ctx := &pipeline.Context{
|
|
Command: "dws contract probe",
|
|
Args: []string{"--" + spec.Name, value},
|
|
FlagSpecs: []pipeline.FlagInfo{spec},
|
|
}
|
|
if err := (handlers.BoolValueHandler{}).Handle(ctx); err != nil {
|
|
t.Fatalf("BoolValueHandler.Handle() error = %v", err)
|
|
}
|
|
want := []string{"--" + spec.Name + "=" + value}
|
|
if !reflect.DeepEqual(ctx.Args, want) {
|
|
t.Fatalf("normalized args = %v, want %v", ctx.Args, want)
|
|
}
|
|
|
|
flags := pflag.NewFlagSet(spec.Name, pflag.ContinueOnError)
|
|
flags.Bool(spec.Name, false, "")
|
|
if err := flags.Parse(ctx.Args); err != nil {
|
|
t.Fatalf("pflag rejected normalized args %v: %v", ctx.Args, err)
|
|
}
|
|
got, err := flags.GetBool(spec.Name)
|
|
if err != nil || got != (value == "true") || !flags.Changed(spec.Name) {
|
|
t.Fatalf("parsed %s = %v, changed=%v, error=%v", spec.Name, got, flags.Changed(spec.Name), err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
t.Logf("verified detached boolean syntax for %d distinct distribution flag contracts", len(keys))
|
|
}
|
|
|
|
func TestBooleanSyntaxPreservesDefaultsRequiredAndChangedContracts(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
path string
|
|
flag string
|
|
value string
|
|
wantDefault string
|
|
wantValue string
|
|
}{
|
|
{name: "root default false", path: "chat bot find", flag: "dry-run", value: "false", wantDefault: "false", wantValue: "false"},
|
|
{name: "root mock default false", path: "chat bot find", flag: "mock", value: "true", wantDefault: "false", wantValue: "true"},
|
|
{name: "local force default false", path: "upgrade", flag: "force", value: "false", wantDefault: "false", wantValue: "false"},
|
|
{name: "local default true", path: "sheet find", flag: "match-case", value: "false", wantDefault: "true", wantValue: "false"},
|
|
{name: "required explicit false", path: "contact dept create", flag: "create-dept-group", value: "false", wantDefault: "false", wantValue: "false"},
|
|
{name: "changed false remains explicit", path: "sheet csv-put", flag: "allow-overwrite", value: "false", wantDefault: "false", wantValue: "false"},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
root := NewSchemaSourceRootCommand()
|
|
leaf := resolveParamLeaf(root, test.path)
|
|
if leaf == nil {
|
|
t.Fatalf("command %q is not runnable", test.path)
|
|
}
|
|
flag := booleanContractFlag(leaf, test.flag)
|
|
if flag == nil || flag.DefValue != test.wantDefault || flag.Changed {
|
|
t.Fatalf("initial --%s contract = %#v, want default %q and unchanged", test.flag, flag, test.wantDefault)
|
|
}
|
|
|
|
pathArgs := strings.Fields(test.path)
|
|
rawArgs := append(append([]string(nil), pathArgs...), "--"+test.flag, test.value)
|
|
ctx, err := pipeline.RunPreParseArgs(root, newPipelineEngine(), rawArgs)
|
|
if err != nil {
|
|
t.Fatalf("RunPreParseArgs(%v) error = %v", rawArgs, err)
|
|
}
|
|
if ctx == nil {
|
|
t.Fatal("RunPreParseArgs returned nil context")
|
|
}
|
|
flagArgs := ctx.Args[len(pathArgs):]
|
|
if err := leaf.ParseFlags(flagArgs); err != nil {
|
|
t.Fatalf("ParseFlags(%v) error = %v", flagArgs, err)
|
|
}
|
|
flag = booleanContractFlag(leaf, test.flag)
|
|
if flag == nil || flag.Value.String() != test.wantValue || !flag.Changed {
|
|
t.Fatalf("final --%s contract = %#v, want value %q and changed", test.flag, flag, test.wantValue)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDetachedDryRunValuesReachTheExpectedFinalDispatchBoundary(t *testing.T) {
|
|
base := []string{
|
|
"mail", "folder", "update",
|
|
"--email", "fixture@example.com", "--id", "folder-1", "--name", "Fixture Folder",
|
|
}
|
|
|
|
bareArgs := append(append([]string(nil), base...), "--dry-run")
|
|
_, barePreview, bareAttempts, bareErr := executeParamAliasDryRunE2E(t, bareArgs...)
|
|
if bareErr != nil || !barePreview.DryRun || barePreview.Executed || len(bareAttempts) != 0 {
|
|
t.Fatalf("bare dry-run = preview:%#v attempts:%#v error:%v", barePreview, bareAttempts, bareErr)
|
|
}
|
|
|
|
trueArgs := append(append([]string(nil), base...), "--dry-run", "TRUE")
|
|
trueCtx, truePreview, trueAttempts, trueErr := executeParamAliasDryRunE2E(t, trueArgs...)
|
|
if trueErr != nil || !reflect.DeepEqual(truePreview, barePreview) || len(trueAttempts) != 0 {
|
|
t.Fatalf("detached true = context:%#v preview:%#v attempts:%#v error:%v", trueCtx, truePreview, trueAttempts, trueErr)
|
|
}
|
|
if !hasBooleanCorrection(trueCtx, "--dry-run TRUE", "--dry-run=true") {
|
|
t.Fatalf("detached true correction = %#v", trueCtx)
|
|
}
|
|
|
|
falseCases := []struct {
|
|
name string
|
|
args []string
|
|
}{
|
|
{name: "detached", args: append(append([]string(nil), base...), "--dry-run", "false")},
|
|
{name: "explicit", args: append(append([]string(nil), base...), "--dry-run=false")},
|
|
}
|
|
var wantAttempts []any
|
|
for _, test := range falseCases {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
ctx, _, attempts, err := executeParamAliasDryRunE2E(t, test.args...)
|
|
if err == nil || !strings.Contains(err.Error(), "dry-run reached the injected command runner") {
|
|
t.Fatalf("dry-run=false dispatch error = %v", err)
|
|
}
|
|
if len(attempts) != 1 || attempts[0].DryRun {
|
|
t.Fatalf("dry-run=false attempts = %#v", attempts)
|
|
}
|
|
if test.name == "detached" && !hasBooleanCorrection(ctx, "--dry-run false", "--dry-run=false") {
|
|
t.Fatalf("detached false correction = %#v", ctx)
|
|
}
|
|
serialized := []any{attempts[0].CanonicalProduct, attempts[0].Tool, attempts[0].Params, attempts[0].DryRun}
|
|
if wantAttempts == nil {
|
|
wantAttempts = serialized
|
|
} else if !reflect.DeepEqual(serialized, wantAttempts) {
|
|
t.Fatalf("detached and explicit false dispatch differ\nwant=%#v\ngot=%#v", wantAttempts, serialized)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestContradictoryBooleanValuesFailBeforeDestructiveDispatch(t *testing.T) {
|
|
caller := ¶mAliasCaptureCaller{}
|
|
ctx, err := executeParamAliasE2E(t, caller,
|
|
"mail", "thread", "trash",
|
|
"--email", "user@example.com", "--id", "conversation-1",
|
|
"--yes", "true", "--yes=false",
|
|
)
|
|
var conflict *pipeline.BoolValueConflictError
|
|
if !stderrors.As(err, &conflict) {
|
|
t.Fatalf("conflicting confirmation error = %v, want BoolValueConflictError (ctx=%#v)", err, ctx)
|
|
}
|
|
if conflict.Flag != "yes" || !reflect.DeepEqual(conflict.Values, []string{"false", "true"}) {
|
|
t.Fatalf("conflict = %#v", conflict)
|
|
}
|
|
if len(caller.calls) != 0 {
|
|
t.Fatalf("conflicting confirmation reached destructive dispatch: %#v", caller.calls)
|
|
}
|
|
}
|
|
|
|
func booleanContractFlag(command *cobra.Command, name string) *pflag.Flag {
|
|
if command == nil {
|
|
return nil
|
|
}
|
|
if flag := command.Flags().Lookup(name); flag != nil {
|
|
return flag
|
|
}
|
|
return command.InheritedFlags().Lookup(name)
|
|
}
|
|
|
|
func hasBooleanCorrection(ctx *pipeline.Context, original, corrected string) bool {
|
|
if ctx == nil {
|
|
return false
|
|
}
|
|
for _, correction := range ctx.Corrections {
|
|
if correction.Handler == "boolvalue" && correction.Original == original && correction.Corrected == corrected {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|