Files
dingtalk-workspace-cli/internal/pipeline/command_fallback_test.go

352 lines
13 KiB
Go

// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0
package pipeline
import (
"errors"
"reflect"
"strings"
"testing"
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/cmdutil"
"github.com/spf13/cobra"
)
func TestCrossPlatformCoverageRunPreParseArgsRewritesReviewedCommandBeforeFlags(t *testing.T) {
root, executed := commandFallbackPipelineRoot()
engine := commandFallbackPipelineEngine(map[string]CommandPathFallback{
"chat +bad": {From: "chat +bad", Mode: "rewrite", To: "chat +good"},
})
raw := []string{"--format", "json", "chat", "+bad", "--query", "project"}
ctx, err := RunPreParseArgs(root, engine, raw)
if err != nil {
t.Fatal(err)
}
wantArgs := []string{"--format", "json", "chat", "+good", "--query", "project"}
if ctx == nil || !reflect.DeepEqual(ctx.Args, wantArgs) || ctx.Command != "dws chat +good" {
t.Fatalf("context = %#v, want args=%v command=dws chat +good", ctx, wantArgs)
}
if len(ctx.Corrections) != 1 || ctx.Corrections[0].Handler != "command-path-fallback" ||
ctx.Corrections[0].Original != "chat +bad" || ctx.Corrections[0].Corrected != "chat +good" {
t.Fatalf("corrections = %#v", ctx.Corrections)
}
if _, err := root.ExecuteC(); err != nil {
t.Fatal(err)
}
if *executed != "dws chat +good:project" {
t.Fatalf("executed = %q", *executed)
}
}
func TestCrossPlatformCoverageRunPreParseArgsRewritesMultiTokenPathAroundPersistentFlags(t *testing.T) {
root, executed := commandFallbackPipelineRoot()
engine := commandFallbackPipelineEngine(map[string]CommandPathFallback{
"chat group search": {From: "chat group search", Mode: "rewrite", To: "chat search"},
})
raw := []string{"chat", "--format", "json", "group", "search", "--query", "project"}
ctx, err := RunPreParseArgs(root, engine, raw)
if err != nil {
t.Fatal(err)
}
wantArgs := []string{"chat", "search", "--format", "json", "--query", "project"}
if ctx == nil || !reflect.DeepEqual(ctx.Args, wantArgs) {
t.Fatalf("rewritten args = %#v, want %v", ctx, wantArgs)
}
if _, err := root.ExecuteC(); err != nil {
t.Fatal(err)
}
if *executed != "dws chat search:project" {
t.Fatalf("executed = %q", *executed)
}
}
func TestCrossPlatformCoverageCommandPathFallbackDefersToExactRunnableCommandAndAlias(t *testing.T) {
if exactRunnableCommandPath(nil, []string{"dws"}) {
t.Fatal("nil root unexpectedly resolved an executable path")
}
emptyRoot := &cobra.Command{Use: "dws"}
if exactRunnableCommandPath(emptyRoot, []string{"dws"}) {
t.Fatal("root-only path unexpectedly resolved as an executable leaf")
}
for _, test := range []struct {
name string
path string
}{
{name: "primary", path: "+members"},
{name: "alias", path: "+runtime-members"},
} {
t.Run(test.name, func(t *testing.T) {
executed := ""
root := &cobra.Command{Use: "dws", SilenceErrors: true, SilenceUsage: true}
chat := &cobra.Command{Use: "chat"}
members := &cobra.Command{
Use: "+members",
Aliases: []string{"+runtime-members"},
RunE: func(cmd *cobra.Command, _ []string) error {
sentinel, _ := cmd.Flags().GetString("sentinel")
executed = cmd.CommandPath() + ":" + sentinel
return nil
},
}
members.Flags().String("sentinel", "", "")
chat.AddCommand(members)
root.AddCommand(chat)
engine := commandFallbackPipelineEngine(map[string]CommandPathFallback{
"chat " + test.path: {From: "chat " + test.path, Mode: "rewrite", To: "chat +good"},
})
args := []string{"chat", test.path, "--sentinel", "fixture"}
root.SetArgs(args)
ctx, err := RunPreParseArgs(root, engine, args)
if err != nil {
t.Fatal(err)
}
if ctx != nil {
t.Fatalf("runtime command unexpectedly entered correction pipeline: %#v", ctx)
}
if _, err := root.ExecuteC(); err != nil {
t.Fatal(err)
}
if executed != "dws chat +members:fixture" {
t.Fatalf("runtime command execution = %q", executed)
}
})
}
}
func TestCrossPlatformCoverageCommandPathFallbackMaySupersedeHintOnlyCommand(t *testing.T) {
root, executed := commandFallbackPipelineRoot()
chat, _, err := root.Find([]string{"chat"})
if err != nil {
t.Fatal(err)
}
chat.AddCommand(cmdutil.HintSubCmd("+bad", "use the reviewed recovery"))
engine := commandFallbackPipelineEngine(map[string]CommandPathFallback{
"chat +bad": {From: "chat +bad", Mode: "rewrite", To: "chat +good"},
})
args := []string{"chat", "+bad", "--query", "project"}
root.SetArgs(args)
ctx, err := RunPreParseArgs(root, engine, args)
if err != nil {
t.Fatal(err)
}
if ctx == nil || ctx.Command != "dws chat +good" || len(ctx.Corrections) != 1 {
t.Fatalf("hint-only fallback context = %#v", ctx)
}
if _, err := root.ExecuteC(); err != nil {
t.Fatal(err)
}
if *executed != "dws chat +good:project" {
t.Fatalf("hint-only fallback execution = %q", *executed)
}
}
func TestCrossPlatformCoverageRunPreParseArgsRejectsAmbiguousCommandFallbackWithoutDispatch(t *testing.T) {
root, executed := commandFallbackPipelineRoot()
engine := commandFallbackPipelineEngine(map[string]CommandPathFallback{
"chat +choose": {
From: "chat +choose",
Mode: "ambiguous",
Candidates: []string{"chat +good", "chat +other"},
},
})
ctx, err := RunPreParseArgs(root, engine, []string{"--format", "json", "chat", "+choose", "--query", "project"})
if ctx == nil {
t.Fatal("ambiguous fallback returned nil context")
}
structured := requireCommandResolutionError(t, err, "ambiguous_command_fallback")
if len(structured.Actions) != 2 || !strings.Contains(structured.Hint, "dws chat +good") || !strings.Contains(structured.Hint, "dws chat +other") {
t.Fatalf("ambiguous recovery = %#v", structured)
}
if *executed != "" {
t.Fatalf("ambiguous fallback dispatched %q", *executed)
}
if format, getErr := root.PersistentFlags().GetString("format"); getErr != nil || format != "json" {
t.Fatalf("format = %q, %v; want primed json", format, getErr)
}
}
func TestCrossPlatformCoverageCommandPathFallbackLeavesUnknownAndCanonicalFlagErrorsDistinct(t *testing.T) {
root, _ := commandFallbackPipelineRoot()
engine := commandFallbackPipelineEngine(map[string]CommandPathFallback{
"chat +bad": {From: "chat +bad", Mode: "rewrite", To: "chat +good"},
})
_, err := RunPreParseArgs(root, engine, []string{"chat", "+missing", "--query", "project"})
requireCommandResolutionError(t, err, "unknown_shortcut")
validRoot, _ := commandFallbackPipelineRoot()
validRaw := []string{"chat", "+good", "--not-a-flag", "value"}
if ctx, err := RunPreParseArgs(validRoot, engine, validRaw); ctx != nil || err != nil {
t.Fatalf("valid command preparse = %#v, %v", ctx, err)
}
validRoot.SetArgs(validRaw)
_, executeErr := validRoot.ExecuteC()
if executeErr == nil || !strings.Contains(executeErr.Error(), "unknown flag") {
t.Fatalf("canonical bad flag error = %v", executeErr)
}
}
func TestCrossPlatformCoverageCommandPathFallbackCannotBypassCanonicalConfirmation(t *testing.T) {
for _, test := range []struct {
name string
args []string
wantDispatch int
wantErr bool
}{
{name: "confirmation missing", args: []string{"chat", "+danger"}, wantErr: true},
{name: "confirmed", args: []string{"chat", "+danger", "--yes"}, wantDispatch: 1},
} {
t.Run(test.name, func(t *testing.T) {
dispatches := 0
root := &cobra.Command{Use: "dws", SilenceErrors: true, SilenceUsage: true}
chat := &cobra.Command{Use: "chat"}
write := &cobra.Command{
Use: "+write",
RunE: func(cmd *cobra.Command, _ []string) error {
confirmed, _ := cmd.Flags().GetBool("yes")
if !confirmed {
return apperrors.NewValidation("confirmation required", apperrors.WithReason("confirmation_required"))
}
dispatches++
return nil
},
}
write.Flags().Bool("yes", false, "")
chat.AddCommand(write)
root.AddCommand(chat)
engine := commandFallbackPipelineEngine(map[string]CommandPathFallback{
"chat +danger": {From: "chat +danger", Mode: "rewrite", To: "chat +write"},
})
if _, err := RunPreParseArgs(root, engine, test.args); err != nil {
t.Fatal(err)
}
_, err := root.ExecuteC()
if (err != nil) != test.wantErr || dispatches != test.wantDispatch {
t.Fatalf("execute error=%v dispatches=%d, want error=%v dispatches=%d", err, dispatches, test.wantErr, test.wantDispatch)
}
})
}
}
func TestCrossPlatformCoverageCommandPathFallbackRejectsInvalidGeneratedMode(t *testing.T) {
root, _ := commandFallbackPipelineRoot()
for _, entry := range []CommandPathFallback{
{From: "chat +bad", Mode: "rewrite"},
{From: "chat +bad", Mode: "invalid", To: "chat +good"},
} {
engine := commandFallbackPipelineEngine(map[string]CommandPathFallback{"chat +bad": entry})
_, err := RunPreParseArgs(root, engine, []string{"chat", "+bad"})
var structured *apperrors.Error
if !errors.As(err, &structured) || structured.Category != apperrors.CategoryInternal {
t.Fatalf("invalid entry error = %T %v", err, err)
}
}
}
func TestCrossPlatformCoverageRunPreParseArgsKeepsCommandErrorWhenPresentationPrimingFails(t *testing.T) {
fallbackRoot, _ := commandFallbackPipelineRoot()
fallbackEngine := commandFallbackPipelineEngine(map[string]CommandPathFallback{
"chat +choose": {
From: "chat +choose",
Mode: "ambiguous",
Candidates: []string{"chat +good", "chat +other"},
},
})
_, err := RunPreParseArgs(fallbackRoot, fallbackEngine, []string{"chat", "+choose", "--format"})
requireCommandResolutionError(t, err, "ambiguous_command_fallback")
resolutionRoot, _ := commandFallbackPipelineRoot()
_, err = RunPreParseArgs(resolutionRoot, nil, []string{"chat", "+missing", "--format"})
requireCommandResolutionError(t, err, "unknown_shortcut")
handlerRoot, _ := commandFallbackPipelineRoot()
handlerEngine := NewEngine()
handlerEngine.Register(newStub("fail-before-presentation", PreParse, func(*Context) error {
return errors.New("handler failure")
}))
ctx, err := RunPreParseArgs(handlerRoot, handlerEngine, []string{"chat", "+good", "--query", "project", "--format"})
if ctx == nil || err == nil || !strings.Contains(err.Error(), "handler failure") {
t.Fatalf("handler error after presentation failure = context %#v, error %v", ctx, err)
}
}
func TestCrossPlatformCoverageCommandFallbackTraversalRemainingBranches(t *testing.T) {
args, positions := argsForCommandTraversalWithPositions(nil, []string{"chat", "+good"})
if !reflect.DeepEqual(args, []string{"chat", "+good"}) || !reflect.DeepEqual(positions, []int{0, 1}) {
t.Fatalf("nil-root traversal = args %v, positions %v", args, positions)
}
root := &cobra.Command{Use: "dws"}
root.PersistentFlags().String("format", "table", "")
raw := []string{"chat", "+good", "--", "--format", "json"}
args, positions = argsForCommandTraversalWithPositions(root, raw)
if !reflect.DeepEqual(args, raw) || !reflect.DeepEqual(positions, []int{0, 1, 2, 3, 4}) {
t.Fatalf("terminator traversal = args %v, positions %v", args, positions)
}
}
func TestCrossPlatformCoverageRewriteCommandPathTokensLeavesArgsWhenRewriteIsEmpty(t *testing.T) {
raw := []string{"chat", "+bad", "--query", "project"}
for name, test := range map[string]struct {
positions []int
tokens []string
}{
"no command positions": {tokens: []string{"chat", "+good"}},
"no canonical tokens": {positions: []int{0, 1}},
} {
t.Run(name, func(t *testing.T) {
got := rewriteCommandPathTokens(raw, test.positions, test.tokens)
if !reflect.DeepEqual(got, raw) {
t.Fatalf("rewriteCommandPathTokens() = %v, want %v", got, raw)
}
got[0] = "mutated"
if raw[0] != "chat" {
t.Fatal("empty rewrite returned aliased argv")
}
})
}
}
func commandFallbackPipelineEngine(entries map[string]CommandPathFallback) *Engine {
engine := NewEngine()
engine.SetCommandPathFallbackLookup(func(path string) (CommandPathFallback, bool) {
entry, ok := entries[path]
return entry, ok
})
return engine
}
func commandFallbackPipelineRoot() (*cobra.Command, *string) {
executed := new(string)
root := &cobra.Command{Use: "dws", SilenceErrors: true, SilenceUsage: true}
root.PersistentFlags().String("format", "table", "")
chat := &cobra.Command{Use: "chat"}
good := &cobra.Command{
Use: "+good",
RunE: func(cmd *cobra.Command, _ []string) error {
query, _ := cmd.Flags().GetString("query")
*executed = cmd.CommandPath() + ":" + query
return nil
},
}
good.Flags().String("query", "", "")
good.Flags().String("keyword", "", "")
other := &cobra.Command{Use: "+other", Run: func(cmd *cobra.Command, _ []string) { *executed = cmd.CommandPath() }}
search := &cobra.Command{
Use: "search",
RunE: func(cmd *cobra.Command, _ []string) error {
query, _ := cmd.Flags().GetString("query")
*executed = cmd.CommandPath() + ":" + query
return nil
},
}
search.Flags().String("query", "", "")
group := &cobra.Command{Use: "group"}
group.AddCommand(cmdutil.HintSubCmd("search", "use dws chat search"))
chat.AddCommand(good, other, search, group)
root.AddCommand(chat)
return root, executed
}