// 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 handlers import ( "errors" "reflect" "testing" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/pipeline" ) // fakeLookup returns a fixed table for the "dws demo cmd" command and ok=false // for anything else, standing in for cli.LookupParamAlias in tests. func fakeLookup(aliases map[string]string, blocked, ambiguous []string) func(string) (map[string]string, []string, []string, bool) { return func(raw string) (map[string]string, []string, []string, bool) { if raw != "dws demo cmd" { return nil, nil, nil, false } return aliases, blocked, ambiguous, true } } func newSemanticHandler() SemanticAliasHandler { return SemanticAliasHandler{ Lookup: fakeLookup( map[string]string{"keyword": "query", "page-size": "limit"}, []string{"count"}, []string{"user-id"}, ), } } func TestSemanticAliasHandlerRewritesSynonym(t *testing.T) { ctx := &pipeline.Context{Command: "dws demo cmd", Args: []string{"--keyword", "hello"}} if err := newSemanticHandler().Handle(ctx); err != nil { t.Fatalf("Handle() error = %v", err) } if want := []string{"--query", "hello"}; !reflect.DeepEqual(ctx.Args, want) { t.Fatalf("Args = %v, want %v", ctx.Args, want) } if len(ctx.Corrections) != 1 || ctx.Corrections[0].Kind != "semantic" || ctx.Corrections[0].Corrected != "--query" { t.Fatalf("correction = %#v", ctx.Corrections) } } func TestSemanticAliasHandlerNameAndPhase(t *testing.T) { h := SemanticAliasHandler{} if h.Name() != "semantic-alias" || h.Phase() != pipeline.PreParse { t.Fatalf("handler identity = %q / %s", h.Name(), h.Phase()) } } func TestSemanticAliasHandlerPreservesEqualsValueSyntax(t *testing.T) { ctx := &pipeline.Context{Command: "dws demo cmd", Args: []string{"--pageSize=50"}} if err := newSemanticHandler().Handle(ctx); err != nil { t.Fatalf("Handle() error = %v", err) } // --pageSize morphs to page-size, which the table aliases to limit. if want := []string{"--limit=50"}; !reflect.DeepEqual(ctx.Args, want) { t.Fatalf("Args = %v, want %v", ctx.Args, want) } } func TestSemanticAliasHandlerLeavesBlockedAndAmbiguous(t *testing.T) { ctx := &pipeline.Context{Command: "dws demo cmd", Args: []string{"--count", "10", "--user-id", "u1"}} if err := newSemanticHandler().Handle(ctx); err != nil { t.Fatalf("Handle() error = %v", err) } if want := []string{"--count", "10", "--user-id", "u1"}; !reflect.DeepEqual(ctx.Args, want) { t.Fatalf("blocked/ambiguous names must not be rewritten: Args = %v, want %v", ctx.Args, want) } if len(ctx.Corrections) != 0 { t.Fatalf("no corrections expected, got %#v", ctx.Corrections) } if ctx.ProtectedFlags["count"] != pipeline.FlagProtectionBlocked || ctx.ProtectedFlags["user-id"] != pipeline.FlagProtectionAmbiguous { t.Fatalf("protections = %#v", ctx.ProtectedFlags) } } func TestSemanticAliasHandlerProtectsWithoutAliases(t *testing.T) { h := SemanticAliasHandler{Lookup: fakeLookup(nil, []string{"limt"}, nil)} ctx := &pipeline.Context{Command: "dws demo cmd", Args: []string{"--limt", "10"}} if err := h.Handle(ctx); err != nil { t.Fatalf("Handle() error = %v", err) } if !ctx.IsFlagProtected("limt") { t.Fatalf("blocked-only entry did not populate pipeline protection: %#v", ctx.ProtectedFlags) } } func TestSemanticAliasHandlerRejectsMixedAliasAndCanonical(t *testing.T) { for _, args := range [][]string{ {"--keyword", "one", "--query", "two"}, {"--query=two", "--keyword=one"}, } { ctx := &pipeline.Context{Command: "dws demo cmd", Args: args} err := newSemanticHandler().Handle(ctx) var conflict *pipeline.FlagConflictError if !errors.As(err, &conflict) { t.Fatalf("Handle(%v) error = %v, want FlagConflictError", args, err) } if conflict.Canonical != "query" || !reflect.DeepEqual(conflict.Spellings, []string{"keyword", "query"}) { t.Fatalf("conflict = %#v", conflict) } } } func TestSemanticAliasHandlerStopsAtDoubleDash(t *testing.T) { ctx := &pipeline.Context{Command: "dws demo cmd", Args: []string{"--query", "one", "--", "--keyword", "two"}} if err := newSemanticHandler().Handle(ctx); err != nil { t.Fatalf("Handle() error = %v", err) } if want := []string{"--query", "one", "--", "--keyword", "two"}; !reflect.DeepEqual(ctx.Args, want) { t.Fatalf("Args = %v, want %v", ctx.Args, want) } } func TestSemanticAliasHandlerNoOpCases(t *testing.T) { h := newSemanticHandler() // Unknown command → Lookup returns ok=false. ctx := &pipeline.Context{Command: "dws other", Args: []string{"--keyword", "x"}} _ = h.Handle(ctx) if !reflect.DeepEqual(ctx.Args, []string{"--keyword", "x"}) || len(ctx.Corrections) != 0 { t.Fatalf("unknown command must be a no-op: %v / %#v", ctx.Args, ctx.Corrections) } // Empty command path. ctx = &pipeline.Context{Command: "", Args: []string{"--keyword", "x"}} _ = h.Handle(ctx) if !reflect.DeepEqual(ctx.Args, []string{"--keyword", "x"}) { t.Fatalf("empty command must be a no-op: %v", ctx.Args) } // Nil Lookup (handler not wired). ctx = &pipeline.Context{Command: "dws demo cmd", Args: []string{"--keyword", "x"}} _ = SemanticAliasHandler{}.Handle(ctx) if !reflect.DeepEqual(ctx.Args, []string{"--keyword", "x"}) { t.Fatalf("nil Lookup must be a no-op: %v", ctx.Args) } // A real flag that also appears nowhere in the table is left alone. ctx = &pipeline.Context{Command: "dws demo cmd", Args: []string{"--query", "x", "--unknown", "y", "positional", "-n"}} _ = h.Handle(ctx) if !reflect.DeepEqual(ctx.Args, []string{"--query", "x", "--unknown", "y", "positional", "-n"}) { t.Fatalf("canonical/positional/short tokens must be untouched: %v", ctx.Args) } } func TestSplitFlagToken(t *testing.T) { cases := []struct { arg string bare string suffix string isFlag bool }{ {"--query", "query", "", true}, {"--limit=50", "limit", "=50", true}, {"--pageSize", "pageSize", "", true}, {"positional", "", "", false}, {"-n", "", "", false}, {"--", "", "", false}, {"--=v", "", "", false}, } for _, c := range cases { bare, suffix, isFlag := splitFlagToken(c.arg) if bare != c.bare || suffix != c.suffix || isFlag != c.isFlag { t.Fatalf("splitFlagToken(%q) = (%q,%q,%v), want (%q,%q,%v)", c.arg, bare, suffix, isFlag, c.bare, c.suffix, c.isFlag) } } }