package commandkit import ( "strings" "testing" "gitea.maximumdirect.net/eric/CommandKit/internal/commandpath" ) func flagAliasCommand(name string, flags ...Flag[struct{}]) Command { return NewCommand(CommandSpec[struct{}]{ Name: name, Summary: name, Flags: flags, Run: func(Invocation, struct{}) Outcome { return Success() }, }) } func stringFlag(name, short string) Flag[struct{}] { return ValueFlag(FlagMeta{Name: name, Short: short}, (*string)(nil), func(value string) (string, error) { return value, nil }, func(*struct{}, Supplied[string]) {}, ) } func TestFlagAliasChainsUseScopedOverridesAndCanonicalEndpoints(t *testing.T) { inspect := flagAliasCommand("inspect", stringFlag("color", "c"), stringFlag("format", "f")) show := flagAliasCommand("show", stringFlag("color", "c"), stringFlag("format", "f")) group := Group("Tools", "tools", show) app, err := New(Identity{Name: "Example"}, inspect, group, Alias("look", "inspect"), FlagAlias("--colour", "--hue"), FlagAlias("--hue", "--color"), FlagAlias("-x", "--color"), FlagAlias("--tint", "-c"), FlagAliasFor("Tools", "--group-help", "--help"), FlagAliasFor("inspect", "--hue", "--format"), FlagAliasFor("inspect", "--local", "--format"), ) if err != nil { t.Fatal(err) } inspectAliases := app.flagAliases[commandpath.Path{"inspect"}.Key()].bySpelling for source, target := range map[string]string{ "--colour": "--format", "--hue": "--format", "-x": "--color", "--tint": "-c", "--local": "--format", } { alias, ok := inspectAliases[flagSpellingKey(capturedSpelling(source))] if !ok || alias.target.token() != target { t.Errorf("inspect alias %s = (%s, %v), want %s", source, alias.target.token(), ok, target) } } groupAliases := app.flagAliases[commandpath.Path{"Tools"}.Key()].bySpelling if _, active := groupAliases[flagSpellingKey(capturedFlagSpelling{name: "colour"})]; active { t.Error("global alias was active on a group without its destination flag") } if _, inherited := groupAliases[flagSpellingKey(capturedFlagSpelling{name: "local"})]; inherited { t.Fatal("command-scoped alias was inherited by its parent group") } if _, inherited := app.flagAliases[commandpath.Path{"Tools", "show"}.Key()].bySpelling[flagSpellingKey(capturedFlagSpelling{name: "local"})]; inherited { t.Fatal("command-scoped alias was inherited by a different command") } groupHelp := flagSpellingKey(capturedFlagSpelling{name: "group-help"}) if got := groupAliases[groupHelp].target.token(); got != "--help" { t.Fatalf("group-scoped help alias target = %s, want --help", got) } if _, inherited := app.flagAliases[commandpath.Path{"Tools", "show"}.Key()].bySpelling[groupHelp]; inherited { t.Fatal("group-scoped alias was inherited by its child command") } } func TestFlagAliasDiscoveryContextsUseOnlyGlobalHelpAliases(t *testing.T) { show := flagAliasCommand("show") app, err := New(Identity{Name: "Example"}, Group("Tools", "tools", show), Alias("shortcut show", "Tools show"), FlagAlias("--hlep", "--help"), FlagAliasFor("Tools show", "--local-help", "--help"), ) if err != nil { t.Fatal(err) } globalHelp := flagSpellingKey(capturedFlagSpelling{name: "hlep"}) if got := app.flagAliases[commandpath.Path(nil).Key()].bySpelling[globalHelp].target.token(); got != "--help" { t.Fatalf("root --hlep target = %s, want --help", got) } if got := app.flagAliases[commandpath.Path{"Tools"}.Key()].bySpelling[globalHelp].target.token(); got != "--help" { t.Fatalf("group --hlep target = %s, want --help", got) } discovery := app.discoveryFlags[commandpath.Path{"shortcut"}.Key()].bySpelling if got := discovery[globalHelp].target.token(); got != "--help" { t.Fatalf("discovery --hlep target = %s, want --help", got) } if _, inherited := discovery[flagSpellingKey(capturedFlagSpelling{name: "local-help"})]; inherited { t.Fatal("discovery context inherited a command-scoped alias") } } func TestFlagAliasDuplicatesAndScopesAreDeterministic(t *testing.T) { command := flagAliasCommand("inspect", stringFlag("color", "c"), stringFlag("format", "f")) duplicate := FlagAlias("--colour", "--color") app, err := New(Identity{Name: "Example"}, command, flagAliasCommand("plain", stringFlag("color", "c")), duplicate, FlagAlias("--colour", "--color"), FlagAliasFor("inspect", "--colour", "--format")) if err != nil { t.Fatalf("identical duplicates and scoped override were rejected: %v", err) } if got := app.flagAliases[commandpath.Path{"inspect"}.Key()].bySpelling[flagSpellingKey(capturedFlagSpelling{name: "colour"})].target.token(); got != "--format" { t.Fatalf("scoped alias target = %s, want --format", got) } if got := app.flagAliases[commandpath.Path{"plain"}.Key()].bySpelling[flagSpellingKey(capturedFlagSpelling{name: "colour"})].target.token(); got != "--color" { t.Fatalf("effective global alias target = %s, want --color", got) } for name, rules := range map[string][]RoutingRule{ "competing global targets": { FlagAlias("--colour", "--color"), FlagAlias("--colour", "--format"), }, "competing scoped targets": { FlagAliasFor("inspect", "--colour", "--color"), FlagAliasFor("inspect", "--colour", "--format"), }, "unknown scope": {FlagAliasFor("missing", "--colour", "--color")}, "missing scoped destination": {FlagAliasFor("inspect", "--colour", "--missing")}, } { t.Run(name, func(t *testing.T) { declarations := []Declaration{command} for _, rule := range rules { declarations = append(declarations, rule) } if _, err := New(Identity{Name: "Example"}, declarations...); err == nil { t.Fatal("New() accepted invalid flag alias rules") } }) } } func TestFlagAliasCyclesIdentifyParticipatingSpellings(t *testing.T) { for name, declarations := range map[string][]Declaration{ "global": { FlagAlias("--first", "--second"), FlagAlias("--second", "--first"), }, "scoped": { flagAliasCommand("inspect"), FlagAliasFor("inspect", "-a", "--second"), FlagAliasFor("inspect", "--second", "-a"), }, } { t.Run(name, func(t *testing.T) { _, err := New(Identity{Name: "Example"}, declarations...) if err == nil { t.Fatal("New() accepted a flag alias cycle") } message := err.Error() for _, spelling := range []string{"--first", "--second"} { if name == "scoped" { continue } if !strings.Contains(message, spelling) { t.Errorf("cycle error %q does not identify %s", message, spelling) } } if name == "scoped" && (!strings.Contains(message, "-a") || !strings.Contains(message, "--second")) { t.Errorf("scoped cycle error %q does not identify both spellings", message) } }) } } func TestInactiveGlobalAliasesActivateAtomicallyWhenDestinationAppears(t *testing.T) { command := flagAliasCommand("inspect", stringFlag("color", "c")) app, err := New(Identity{Name: "Example"}, command, FlagAlias("--color", "--missing"), FlagAlias("--colour", "--color"), ) if err != nil { t.Fatal(err) } aliases := app.flagAliases[commandpath.Path{"inspect"}.Key()].bySpelling if _, active := aliases[flagSpellingKey(capturedFlagSpelling{name: "color"})]; active { t.Fatal("global alias with an unavailable destination became active") } if got := aliases[flagSpellingKey(capturedFlagSpelling{name: "colour"})].target.token(); got != "--color" { t.Fatalf("--colour endpoint = %s, want canonical --color", got) } snapshot := app.routeSnapshot() commands := len(app.commands) rules := len(app.routingRules) originalAlias := aliases[flagSpellingKey(capturedFlagSpelling{name: "colour"})] if err := app.Register(flagAliasCommand("later", stringFlag("color", "c"), stringFlag("missing", ""))); err == nil { t.Fatal("Register() accepted a newly active global source collision") } currentAlias := app.flagAliases[commandpath.Path{"inspect"}.Key()].bySpelling[flagSpellingKey(capturedFlagSpelling{name: "colour"})] if app.routeSnapshot() != snapshot || len(app.commands) != commands || len(app.routingRules) != rules || currentAlias != originalAlias { t.Fatal("failed registration changed the existing command or routing state") } } func capturedSpelling(token string) capturedFlagSpelling { issues := []error(nil) return captureFlagSpelling("test", token, &issues) }