352 lines
13 KiB
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
|
|
}
|