package commandkit import ( "bytes" "context" "errors" "fmt" "io" "reflect" "strings" "testing" ) func TestDiscoveryUsageRetainsEnteredPrefix(t *testing.T) { initialized := false inspect := NewCommand(CommandSpec[struct{}]{ Name: "inspect", NewInput: func() struct{} { initialized = true; return struct{}{} }, Run: func(Invocation, struct{}) Outcome { t.Fatal("usage failure executed a handler"); return Success() }, }) app, err := New(Identity{Name: "Demo"}, inspect, Group("box", "Box"), Alias("list things", "inspect"), Alias("p", "q"), Alias("q", "box"), Alias("q admin pick", "inspect"), FlagAlias("--hlep", "--help")) if err != nil { t.Fatal(err) } for _, prefix := range []string{"list", "p admin"} { for _, test := range []struct { name string tail []string want string }{ {name: "bare", want: "incomplete command path"}, {name: "unknown flag", tail: []string{"--missing"}, want: "--missing"}, {name: "invalid help", tail: []string{"--hlep=bad"}, want: "for --hlep"}, } { t.Run(prefix+"/"+test.name, func(t *testing.T) { var output, diagnostics bytes.Buffer outcome := app.Execute(context.Background(), append(strings.Fields(prefix), test.tail...), Streams{Out: &output, Err: &diagnostics}) if outcome.Kind != OutcomeUsage || outcome.Status != 2 || outcome.Presentation.Diagnostics.State != DeliveryComplete || output.Len() != 0 { t.Fatalf("outcome = %#v, output %q", outcome, output.String()) } for _, fragment := range []string{test.want, "Usage: Demo " + prefix} { if !strings.Contains(diagnostics.String(), fragment) { t.Errorf("diagnostics %q omit %q", diagnostics.String(), fragment) } } if strings.Contains(diagnostics.String(), "Alias for:") { t.Errorf("discovery was assigned a canonical identity: %q", diagnostics.String()) } }) } } for _, prefix := range []string{"", "inspect"} { var diagnostics bytes.Buffer outcome := app.Execute(context.Background(), append(strings.Fields(prefix), "--missing"), Streams{Err: &diagnostics}) if outcome.Kind != OutcomeUsage || !strings.Contains(diagnostics.String(), "Usage: Demo "+prefix) || strings.Contains(diagnostics.String(), "Alias for:") { t.Errorf("ordinary usage for %q = %#v, diagnostics %q", prefix, outcome, diagnostics.String()) } } if initialized { t.Fatal("usage initialized command input") } } func TestParserFailuresRetainAliasUsageAndFlagSpellings(t *testing.T) { built := 0 command := NewCommand(CommandSpec[struct{}]{ Name: "inspect", NewInput: func() struct{} { built++; return struct{}{} }, Flags: []Flag[struct{}]{ValueFlag(FlagMeta{Name: "format"}, (*string)(nil), func(value string) (string, error) { return value, nil }, func(*struct{}, Supplied[string]) {})}, Run: func(Invocation, struct{}) Outcome { t.Fatal("invalid input executed"); return Success() }, }) app, err := New(Identity{Name: "Demo"}, command, Alias("i", "inspect"), FlagAliasFor("inspect", "--fmt", "--format"), FlagAliasFor("inspect", "-F", "--format"), FlagAlias("--hlep", "--help")) if err != nil { t.Fatal(err) } for _, test := range []struct { args []string spelling string }{ {[]string{"i", "--fmt"}, ": --fmt"}, {[]string{"i", "-F"}, ": -F"}, {[]string{"i", "--hlep=bad", "--help=true"}, "for --hlep"}, {[]string{"i", "-h=bad"}, "for -h"}, } { t.Run(strings.Join(test.args, " "), func(t *testing.T) { var diagnostics bytes.Buffer outcome := app.Execute(context.Background(), test.args, Streams{Out: io.Discard, Err: &diagnostics}) if outcome.Kind != OutcomeUsage || built != 0 { t.Fatalf("outcome = %#v, inputs built = %d", outcome, built) } for _, fragment := range []string{test.spelling, "Usage: Demo i", "Alias for: Demo inspect"} { if !strings.Contains(diagnostics.String(), fragment) { t.Errorf("diagnostics %q omit %q", diagnostics.String(), fragment) } } }) } } func TestCommandContextSurvivesIncompleteAndInvalidOperandCounts(t *testing.T) { for _, spelling := range []string{"run", "r"} { for _, test := range []struct { name string args []string help bool usage bool }{ {name: "interspersed alias", args: []string{"first", "--fmt=yaml", "second"}}, {name: "interspersed canonical flag", args: []string{"first", "--format=yaml", "second"}}, {name: "partial help", args: []string{"first", "--help"}, help: true}, {name: "partial help with flag alias", args: []string{"first", "--fmt=yaml", "--help"}, help: true}, {name: "excess operands", args: []string{"first", "second", "third", "--fmt=yaml"}, usage: true}, {name: "missing operands", args: []string{"first", "--fmt=yaml"}, usage: true}, } { t.Run(spelling+"/"+test.name, func(t *testing.T) { type input struct { operands []string format Supplied[string] } built, ran := 0, false command := NewCommand(CommandSpec[input]{ Name: "run", NewInput: func() input { built++; return input{} }, Flags: []Flag[input]{ValueFlag(FlagMeta{Name: "format"}, (*string)(nil), func(value string) (string, error) { return value, nil }, func(target *input, value Supplied[string]) { target.format = value })}, Arguments: []Argument[input]{RepeatArgument(ArgumentMeta{Name: "ARG"}, 2, 2, func(value string) (string, error) { return value, nil }, func(target *input, value Supplied[[]string]) { target.operands = value.Value })}, Run: func(inv Invocation, value input) Outcome { ran = true if inv.EnteredPath() != spelling || inv.CanonicalPath() != "run" { t.Errorf("invocation paths = %q / %q", inv.EnteredPath(), inv.CanonicalPath()) } if !reflect.DeepEqual(value.operands, []string{"first", "second"}) || !value.format.Set || value.format.Value != "yaml" { t.Errorf("input = %#v", value) } return Success() }, }) app, err := New(Identity{Name: "Demo"}, command, Alias("r", "run"), FlagAliasFor("run", "--fmt", "--format")) if err != nil { t.Fatal(err) } var output, diagnostics bytes.Buffer outcome := app.Execute(context.Background(), append([]string{spelling}, test.args...), Streams{Out: &output, Err: &diagnostics}) if test.usage { if outcome.Kind != OutcomeUsage || strings.Contains(diagnostics.String(), "unknown flag") || !strings.Contains(diagnostics.String(), "Demo "+spelling+" ARG") { t.Fatalf("invalid arity outcome = %v, diagnostics %q", outcome.Kind, diagnostics.String()) } } else if outcome.Kind != OutcomeSuccess { t.Fatalf("outcome = %#v, diagnostics %q", outcome, diagnostics.String()) } if test.help && (!strings.Contains(output.String(), "Demo "+spelling+" ARG") || !strings.Contains(output.String(), "--fmt → --format")) { t.Errorf("command help = %q", output.String()) } if test.help || test.usage { if built != 0 || ran { t.Fatal("help or invalid arity initialized input or ran a handler") } } else if built != 1 || !ran { t.Fatal("valid invocation did not run once") } }) } } } func TestRunnableCommandCanHaveAliasOnlyDiscoveryPrefixes(t *testing.T) { ran := false app, err := New(Identity{Name: "Demo"}, NewCommand(CommandSpec[struct{}]{Name: "box", Run: func(Invocation, struct{}) Outcome { ran = true; return Success() }}), simpleRunnable("inspect"), Alias("box admin pick", "inspect")) if err != nil { t.Fatal(err) } var output bytes.Buffer outcome := app.Execute(context.Background(), []string{"box", "admin", "--help"}, Streams{Out: &output, Err: io.Discard}) if outcome.Kind != OutcomeSuccess || !strings.Contains(output.String(), "pick → inspect") || ran { t.Fatalf("discovery outcome = %v, help %q, ran %t", outcome.Kind, output.String(), ran) } } func TestExplicitHelpRequiresAnExactResolvedTarget(t *testing.T) { initialized := false run := NewCommand(CommandSpec[struct{}]{ Name: "run", NewInput: func() struct{} { initialized = true; return struct{}{} }, Arguments: []Argument[struct{}]{RepeatArgument(ArgumentMeta{Name: "ARG"}, 2, 2, func(value string) (string, error) { return value, nil }, func(*struct{}, Supplied[[]string]) {})}, Run: func(Invocation, struct{}) Outcome { t.Error("help ran the command"); return Success() }, }) app, err := New(Identity{Name: "Demo"}, run, Group("box", "Box", simpleRunnable("show")), Alias("r", "run"), Alias("b", "box"), Alias("list show", "box show")) if err != nil { t.Fatal(err) } for _, args := range [][]string{ {"help", "run", "first", "second"}, {"help", "r", "first"}, {"help", "r", "first", "second"}, {"help", "r", "first", "second", "third"}, } { var output, diagnostics bytes.Buffer outcome := app.Execute(context.Background(), args, Streams{Out: &output, Err: &diagnostics}) if outcome.Kind != OutcomeUsage || output.Len() != 0 { t.Errorf("Execute(%q) = %v, output %q, diagnostics %q", args, outcome.Kind, output.String(), diagnostics.String()) } } for _, args := range [][]string{ {"help", "run"}, {"help", "r"}, {"help", "box"}, {"help", "b"}, {"help", "list"}, {"r", "first", "second", "--help"}, } { var output, diagnostics bytes.Buffer outcome := app.Execute(context.Background(), args, Streams{Out: &output, Err: &diagnostics}) if outcome.Kind != OutcomeSuccess || !strings.Contains(output.String(), "Usage:") { t.Errorf("Execute(%q) = %v, output %q, diagnostics %q", args, outcome.Kind, output.String(), diagnostics.String()) } } if initialized { t.Fatal("help initialized command input") } } func TestDiscoveryHelpComposesGroupShortcutsAndOverrides(t *testing.T) { for _, override := range []bool{false, true} { t.Run(fmt.Sprint(override), func(t *testing.T) { initialized := false inspect := NewCommand(CommandSpec[struct{}]{ Name: "inspect", NewInput: func() struct{} { initialized = true; return struct{}{} }, Flags: []Flag[struct{}]{ValueFlag(FlagMeta{Name: "format"}, (*string)(nil), func(value string) (string, error) { return value, nil }, func(*struct{}, Supplied[string]) {})}, Run: func(Invocation, struct{}) Outcome { return Success() }, }) declarations := []Declaration{Group("box", "Box"), inspect, simpleRunnable("alternate"), Alias("pk", "box"), Alias("box admin pick", "inspect"), Alias("box admin other", "inspect"), FlagAlias("--hlep", "--help"), FlagAliasFor("inspect", "--fmt", "--format")} wantPick := "pick → inspect" if override { declarations = append(declarations, Alias("pk admin pick", "alternate"), Alias("pk admin nested choose", "inspect")) wantPick = "pick → alternate" } app, err := New(Identity{Name: "Demo"}, declarations...) if err != nil { t.Fatal(err) } for _, args := range [][]string{{"pk", "admin", "--help"}, {"help", "pk", "admin"}, {"pk", "admin", "--hlep"}} { var output, diagnostics bytes.Buffer outcome := app.Execute(context.Background(), args, Streams{Out: &output, Err: &diagnostics}) if outcome.Kind != OutcomeSuccess { t.Fatalf("Execute(%q) = %v, diagnostics %q", args, outcome.Kind, diagnostics.String()) } for _, fragment := range []string{"Demo pk admin [command]", wantPick, "other → inspect"} { if !strings.Contains(output.String(), fragment) { t.Errorf("help %q omits %q", output.String(), fragment) } } if override && strings.Contains(output.String(), "pick → inspect") { t.Errorf("help displays overridden target: %q", output.String()) } if strings.Contains(output.String(), "--fmt") { t.Errorf("discovery inherited a descendant flag: %q", output.String()) } } if initialized { t.Fatal("discovery initialized application input") } if override { var output bytes.Buffer outcome := app.Execute(context.Background(), []string{"help", "pk", "admin", "nested"}, Streams{Out: &output, Err: io.Discard}) if outcome.Kind != OutcomeSuccess || !strings.Contains(output.String(), "choose → inspect") { t.Fatalf("entered-only discovery outcome = %v, help %q", outcome.Kind, output.String()) } } var diagnostics bytes.Buffer outcome := app.Execute(context.Background(), []string{"pk", "admin"}, Streams{Out: io.Discard, Err: &diagnostics}) if outcome.Kind != OutcomeUsage || !strings.Contains(diagnostics.String(), "pk admin") { t.Fatalf("bare discovery = %v, diagnostics %q", outcome.Kind, diagnostics.String()) } outcome = app.Execute(context.Background(), []string{"pk", "admin", "--fmt=yaml", "--hlep"}, Streams{Out: io.Discard, Err: io.Discard}) if outcome.Kind != OutcomeUsage { t.Fatal("discovery accepted descendant flag") } outcome = app.Execute(context.Background(), []string{"pk", "admin", "other"}, Streams{Out: io.Discard, Err: io.Discard}) if outcome.Kind != OutcomeSuccess || !initialized { t.Fatal("complete composed route is unavailable") } }) } } func TestHelpDiscoversRoutesThroughIntermediateAliases(t *testing.T) { for _, test := range []struct { name string nested bool override bool expanded bool }{ {name: "group"}, {name: "discovery", nested: true}, {name: "discovery override", nested: true, override: true}, {name: "expanded chain", nested: true, expanded: true}, } { t.Run(test.name, func(t *testing.T) { built, runs := 0, 0 var canonical, entered string makeCommand := func(name string) Command { return NewCommand(CommandSpec[struct{}]{ Name: name, NewInput: func() struct{} { built++; return struct{}{} }, Flags: []Flag[struct{}]{ValueFlag(FlagMeta{Name: "format"}, (*string)(nil), func(value string) (string, error) { return value, nil }, func(*struct{}, Supplied[string]) {})}, Run: func(inv Invocation, _ struct{}) Outcome { runs++ entered, canonical = inv.EnteredPath(), inv.CanonicalPath() return Success() }, }) } group, target := Group("box", "Box"), "box" if test.expanded { group, target = Group("outer", "Outer", group), "outer box" } prefix, middle := "p", "q" if test.nested { prefix, middle = "p admin", "q admin" } declarations := []Declaration{group, makeCommand("inspect"), makeCommand("alternate"), Alias("p", "q"), Alias("q", target), Alias(middle+" pick", "inspect"), Alias(middle+" other", "inspect"), FlagAlias("--hlep", "--help"), FlagAliasFor("inspect", "--fmt", "--format")} wantTarget := "inspect" if test.override { declarations = append(declarations, Alias(prefix+" pick", "alternate")) wantTarget = "alternate" } app, err := New(Identity{Name: "Demo"}, declarations...) if err != nil { t.Fatal(err) } for _, args := range [][]string{ append(strings.Fields(prefix), "--help"), append([]string{"help"}, strings.Fields(prefix)...), append(strings.Fields(prefix), "--hlep"), } { var output, diagnostics bytes.Buffer outcome := app.Execute(context.Background(), args, Streams{Out: &output, Err: &diagnostics}) if outcome.Kind != OutcomeSuccess { t.Errorf("Execute(%q) = %v, diagnostics %q", args, outcome.Kind, diagnostics.String()) } for _, fragment := range []string{"Demo " + prefix, "pick → " + wantTarget, "other → inspect", "--hlep → --help"} { if !strings.Contains(output.String(), fragment) { t.Errorf("help %q omits %q", output.String(), fragment) } } if strings.Contains(output.String(), "--fmt") || strings.Contains(output.String(), "--format") || (test.override && strings.Contains(output.String(), "pick → inspect")) { t.Errorf("help exposes a descendant flag or overridden destination: %q", output.String()) } } if built != 0 || runs != 0 { t.Fatalf("help built %d inputs and ran %d handlers", built, runs) } for _, suffix := range []string{"pick", "other"} { outcome := app.Execute(context.Background(), strings.Fields(prefix+" "+suffix), Streams{Out: io.Discard, Err: io.Discard}) want := "inspect" if suffix == "pick" { want = wantTarget } if outcome.Kind != OutcomeSuccess || canonical != want || entered != prefix+" "+suffix { t.Errorf("route %s %s: outcome %v, entered %q, canonical %q", prefix, suffix, outcome.Kind, entered, canonical) } } }) } } func TestDiscoveryHelpThroughExpandedGroupPath(t *testing.T) { app, err := New(Identity{Name: "Demo"}, Group("outer", "Outer", Group("box", "Box")), simpleRunnable("inspect"), Alias("pk", "outer box"), Alias("outer box admin pick", "inspect")) if err != nil { t.Fatal(err) } for _, args := range [][]string{{"pk", "admin", "--help"}, {"help", "pk", "admin"}} { var output bytes.Buffer outcome := app.Execute(context.Background(), args, Streams{Out: &output, Err: io.Discard}) if outcome.Kind != OutcomeSuccess || !strings.Contains(output.String(), "Demo pk admin [command]") || !strings.Contains(output.String(), "pick → inspect") { t.Fatalf("expanded discovery outcome = %v, help %q", outcome.Kind, output.String()) } } } func TestEmptyCanonicalGroupHelpListsAliasRoutes(t *testing.T) { app, err := New(Identity{Name: "Demo"}, Group("box", "Box"), Group("quiet", "Quiet"), simpleRunnable("inspect"), Alias("box pick", "inspect"), Alias("pk", "box")) if err != nil { t.Fatal(err) } for _, args := range [][]string{{"box", "--help"}, {"help", "box"}, {"pk", "--help"}, {"help", "pk"}} { var output bytes.Buffer outcome := app.Execute(context.Background(), args, Streams{Out: &output, Err: io.Discard}) if outcome.Kind != OutcomeSuccess || !strings.Contains(output.String(), "pick → inspect") { t.Errorf("Execute(%q) = %v, help %q", args, outcome.Kind, output.String()) } } var output bytes.Buffer outcome := app.Execute(context.Background(), []string{"quiet", "--help"}, Streams{Out: &output, Err: io.Discard}) if outcome.Kind != OutcomeSuccess || strings.Contains(output.String(), "Command Aliases:") { t.Fatalf("unaliased group help = %v, %q", outcome.Kind, output.String()) } if outcome := app.Execute(context.Background(), []string{"box", "pick"}, Streams{Out: io.Discard, Err: io.Discard}); outcome.Kind != OutcomeSuccess { t.Fatal("alias route became unavailable") } } func routeTestApp(t *testing.T, extra ...Declaration) *App { t.Helper() collections := simpleRunnable("collections") list := Group("list", "List", collections) testCommand := runnableWithOperands("test", 0, 1) promptkit := Group("promptkit", "PromptKit", list, testCommand) run := runnableWithOperands("run", 0, 1) inspect := runnableWithOperands("inspect", 1, 1) declarations := []Declaration{ promptkit, run, inspect, Alias("collections", "list collections"), Alias("list collections", "promptkit list collections"), Alias("pk", "promptkit"), Alias("promptkit list bundles", "promptkit list collections"), Alias("smoke", "promptkit test"), Alias("r", "run"), Alias("r full", "inspect"), } declarations = append(declarations, extra...) app, err := New(Identity{Name: "Example"}, declarations...) if err != nil { t.Fatal(err) } return app } func TestEnteredRoutesResolveChainsAndPreserveTokenOrigins(t *testing.T) { app := routeTestApp(t) snapshot := app.routeSnapshot() for _, test := range []struct { name string args []string entered string canonical string wantArgs []string wantKind routeKind }{ { name: "command alias chain", args: []string{"collections", "--tag", "first"}, entered: "collections", canonical: "promptkit list collections", wantArgs: []string{"promptkit", "list", "collections", "--tag", "first"}, wantKind: routeCanonical, }, { name: "group alias composition", args: []string{"pk", "list", "bundles"}, entered: "pk list bundles", canonical: "promptkit list collections", wantArgs: []string{"promptkit", "list", "collections"}, wantKind: routeCanonical, }, { name: "smoke operand", args: []string{"smoke", "all"}, entered: "smoke", canonical: "promptkit test", wantArgs: []string{"promptkit", "test", "all"}, wantKind: routeCanonical, }, { name: "explicit longer alternate spelling", args: []string{"r", "full"}, entered: "r full", canonical: "inspect", wantArgs: []string{"inspect"}, wantKind: routeCanonical, }, { name: "canonical runnable operand", args: []string{"run", "full"}, entered: "run", canonical: "run", wantArgs: []string{"run", "full"}, wantKind: routeCanonical, }, { name: "single dash remains an operand", args: []string{"run", "-"}, entered: "run", canonical: "run", wantArgs: []string{"run", "-"}, wantKind: routeCanonical, }, { name: "unknown path", args: []string{"missing", "route"}, entered: "missing route", wantArgs: []string{"missing", "route"}, wantKind: routeUnknown, }, { name: "space-containing token is not split", args: []string{"collections list"}, entered: "collections list", wantArgs: []string{"collections list"}, wantKind: routeUnknown, }, { name: "flag values and spaced tokens", args: []string{"collections", "--message", "--hlep", "--text=two words"}, entered: "collections", canonical: "promptkit list collections", wantArgs: []string{"promptkit", "list", "collections", "--message", "--hlep", "--text=two words"}, wantKind: routeCanonical, }, { name: "terminator stops command matching", args: []string{"collections", "--", "r", "full"}, entered: "collections", canonical: "promptkit list collections", wantArgs: []string{"promptkit", "list", "collections", "--", "r", "full"}, wantKind: routeCanonical, }, } { t.Run(test.name, func(t *testing.T) { got := resolveEnteredRoute(test.args, snapshot) if got.kind != test.wantKind { t.Fatalf("route kind = %d, want %d", got.kind, test.wantKind) } if got.enteredPath.String() != test.entered { t.Errorf("entered path = %q, want %q", got.enteredPath.String(), test.entered) } if got.canonicalPath.String() != test.canonical { t.Errorf("canonical path = %q, want %q", got.canonicalPath.String(), test.canonical) } if !reflect.DeepEqual(got.args, test.wantArgs) { t.Errorf("routed args = %q, want %q", got.args, test.wantArgs) } }) } } func TestLongerRouteSelectionDoesNotFallBackAfterTargetParsingFails(t *testing.T) { app := routeTestApp(t) result := resolveEnteredRoute([]string{"r", "full"}, app.routeSnapshot()) if got, want := result.canonicalPath.String(), "inspect"; got != want { t.Fatalf("canonical path = %q, want %q", got, want) } var diagnostics bytes.Buffer outcome := app.Execute(context.Background(), []string{"r", "full"}, Streams{Out: io.Discard, Err: &diagnostics}) if outcome.Kind != OutcomeUsage { t.Fatalf("outcome kind = %v, want usage; diagnostics: %s", outcome.Kind, diagnostics.String()) } } func TestEnteredAliasCanSelectExplicitLongerMapping(t *testing.T) { app := routeTestApp(t, Alias("pk list bundles", "inspect")) result := resolveEnteredRoute([]string{"pk", "list", "bundles"}, app.routeSnapshot()) if got, want := result.canonicalPath.String(), "inspect"; got != want { t.Fatalf("canonical path = %q, want %q", got, want) } if got, want := result.enteredPath.String(), "pk list bundles"; got != want { t.Fatalf("entered path = %q, want %q", got, want) } } func TestExecuteDispatchesEnteredAliasToCanonicalRunner(t *testing.T) { runs := 0 command := NewCommand(CommandSpec[struct{}]{ Name: "canonical", Summary: "Canonical", Run: func(Invocation, struct{}) Outcome { runs++ return Success() }, }) app, err := New(Identity{Name: "Example"}, command, Alias("shortcut", "canonical")) if err != nil { t.Fatal(err) } outcome := app.Execute(context.Background(), []string{"shortcut"}, Streams{Out: io.Discard, Err: io.Discard}) if outcome.Kind != OutcomeSuccess || runs != 1 { t.Fatalf("alias execution = (%v, runs %d), want success and one canonical run", outcome.Kind, runs) } } func TestInvocationReportsEnteredAndCanonicalPathsPerCall(t *testing.T) { type input struct { label Supplied[string] values Supplied[[]string] } type observed struct { entered string canonical string label string values []string } var calls []observed command := NewCommand(CommandSpec[input]{ Name: "run", Flags: []Flag[input]{ValueFlag(FlagMeta{Name: "label"}, (*string)(nil), func(value string) (string, error) { return value, nil }, func(target *input, label Supplied[string]) { target.label = label }, )}, Arguments: []Argument[input]{RepeatArgument(ArgumentMeta{Name: "value"}, 0, -1, func(value string) (string, error) { return value, nil }, func(target *input, values Supplied[[]string]) { target.values = values }, )}, Run: func(inv Invocation, value input) Outcome { calls = append(calls, observed{ entered: inv.EnteredPath(), canonical: inv.CanonicalPath(), label: value.label.Value, values: append([]string(nil), value.values.Value...), }) return Success() }, }) shortcut := Alias("shortcut", "workspace run") app, err := New(Identity{Name: "Example"}, Group("workspace", "Workspace", command), shortcut) if err != nil { t.Fatal(err) } for _, args := range [][]string{{"workspace", "run", "--label=first", "first"}, {"shortcut", "--label=second", "second"}, {"workspace", "run"}} { if outcome := app.Execute(context.Background(), args, Streams{Out: io.Discard, Err: io.Discard}); outcome.Kind != OutcomeSuccess { t.Fatalf("Execute(%q) = %v, want success", args, outcome.Kind) } } if len(calls) != 3 { t.Fatalf("handler calls = %d, want 3", len(calls)) } want := []observed{ {entered: "workspace run", canonical: "workspace run", label: "first", values: []string{"first"}}, {entered: "shortcut", canonical: "workspace run", label: "second", values: []string{"second"}}, {entered: "workspace run", canonical: "workspace run"}, } if !reflect.DeepEqual(calls, want) { t.Fatalf("handler invocations = %#v, want %#v", calls, want) } secondApp, err := New(Identity{Name: "Another"}, Group("workspace", "Workspace", command), shortcut) if err != nil { t.Fatal(err) } if outcome := secondApp.Execute(context.Background(), []string{"shortcut"}, Streams{Out: io.Discard, Err: io.Discard}); outcome.Kind != OutcomeSuccess { t.Fatalf("reused declaration execution = %v, want success", outcome.Kind) } if got := calls[3]; got.entered != "shortcut" || got.canonical != "workspace run" || len(got.values) != 0 { t.Fatalf("reused declaration invocation = %#v", got) } var zero Invocation if zero.EnteredPath() != "" || zero.CanonicalPath() != "" { t.Fatalf("zero Invocation paths = (%q, %q), want empty paths", zero.EnteredPath(), zero.CanonicalPath()) } } func TestPrivateCommandCallbacksReceiveResolvedInvocationPaths(t *testing.T) { var calls []Invocation command := Command{record: &commandRecord{ name: "inspect", payload: typedCommand[struct{}]{ spec: CommandSpec[struct{}]{Name: "inspect"}, privateRun: func(exec commandExecution, _ struct{}) Outcome { calls = append(calls, exec.invocation) return Success() }, }, }} app, err := New(Identity{Name: "Example"}, command, Alias("shortcut", "inspect")) if err != nil { t.Fatal(err) } for _, args := range [][]string{{"inspect"}, {"shortcut"}} { if outcome := app.Execute(context.Background(), args, Streams{Out: io.Discard, Err: io.Discard}); outcome.Kind != OutcomeSuccess { t.Fatalf("Execute(%q) = %v, want success", args, outcome.Kind) } } if len(calls) != 2 || calls[0].EnteredPath() != "inspect" || calls[0].CanonicalPath() != "inspect" || calls[1].EnteredPath() != "shortcut" || calls[1].CanonicalPath() != "inspect" { t.Fatalf("private callback invocations = %#v", calls) } } func TestExecuteAppliesCanonicalCommandFlagAliasesAfterRouting(t *testing.T) { type input struct{ color string } var got string command := NewCommand(CommandSpec[input]{ Name: "inspect", Flags: []Flag[input]{ValueFlag(FlagMeta{Name: "color", Short: "c"}, (*string)(nil), func(value string) (string, error) { return value, nil }, func(target *input, supplied Supplied[string]) { target.color = supplied.Value }, )}, Run: func(_ Invocation, value input) Outcome { got = value.color return Success() }, }) app, err := New(Identity{Name: "Example"}, command, Alias("shortcut", "inspect"), FlagAliasFor("inspect", "--colour", "-c"), ) if err != nil { t.Fatal(err) } outcome := app.Execute(context.Background(), []string{"shortcut", "--colour=blue"}, Streams{Out: io.Discard, Err: io.Discard}) if outcome.Kind != OutcomeSuccess || got != "blue" { t.Fatalf("alias flag execution = (%v, color %q), want success and blue", outcome.Kind, got) } } func TestAliasConversionErrorsRetainOccurrenceAndEnteredUsage(t *testing.T) { colorFailure := errors.New("invalid color") tagFailure := errors.New("invalid tag") type input struct { color Supplied[string] tags Supplied[[]string] } command := NewCommand(CommandSpec[input]{ Name: "inspect", Flags: []Flag[input]{ ValueFlag(FlagMeta{Name: "color"}, (*string)(nil), func(value string) (string, error) { if value == "bad" { return "", colorFailure } return value, nil }, func(target *input, value Supplied[string]) { target.color = value }), RepeatFlag(FlagMeta{Name: "tag"}, nil, func(value string) (string, error) { if value == "bad" { return "", tagFailure } return value, nil }, func(target *input, value Supplied[[]string]) { target.tags = value }), }, Run: func(Invocation, input) Outcome { return Success() }, }) app, err := New(Identity{Name: "Example"}, command, Alias("shortcut", "inspect"), FlagAliasFor("inspect", "--colour", "--color"), FlagAliasFor("inspect", "--labels", "--tag"), ) if err != nil { t.Fatal(err) } for _, test := range []struct { name string args []string cause error binding string spelling string occurrence int }{ { name: "invalid scalar before valid overwrite", args: []string{"shortcut", "--colour=bad", "--color=good"}, cause: colorFailure, binding: "color", spelling: "--colour", occurrence: 1, }, { name: "invalid repeat occurrence after mixed spelling", args: []string{"shortcut", "--labels=first", "--tag=bad"}, cause: tagFailure, binding: "tag", spelling: "--tag", occurrence: 2, }, } { t.Run(test.name, func(t *testing.T) { var diagnostics bytes.Buffer outcome := app.Execute(context.Background(), test.args, Streams{Out: io.Discard, Err: &diagnostics}) if outcome.Kind != OutcomeUsage { t.Fatalf("outcome kind = %v, want usage", outcome.Kind) } if !errors.Is(outcome.Cause, test.cause) { t.Fatalf("outcome cause = %v, want wrapped %v", outcome.Cause, test.cause) } var conversionErr *flagConversionError if !errors.As(outcome.Cause, &conversionErr) { t.Fatalf("outcome cause %T does not expose flag conversion context", outcome.Cause) } if conversionErr.binding != test.binding || conversionErr.spelling != test.spelling || conversionErr.occurrence != test.occurrence { t.Fatalf("conversion error = %#v, want binding/spelling/occurrence for %s %d", conversionErr, test.spelling, test.occurrence) } for _, fragment := range []string{fmt.Sprintf("flag %s occurrence %d: %s", test.spelling, test.occurrence, test.cause), "Usage: Example shortcut", "Alias for: Example inspect"} { if !strings.Contains(diagnostics.String(), fragment) { t.Errorf("diagnostics %q do not contain %q", diagnostics.String(), fragment) } } }) } } func TestAliasUsageSurvivesRegistrationDuringExecution(t *testing.T) { for _, test := range []struct { name string register func(*App) error wantErr bool }{ { name: "successful registration", register: func(app *App) error { return app.Register(NewCommand(CommandSpec[struct{}]{Name: "late", Run: func(Invocation, struct{}) Outcome { return Success() }})) }, }, { name: "failed registration", register: func(app *App) error { return app.Register(Alias("shortcut", "other")) }, wantErr: true, }, } { t.Run(test.name, func(t *testing.T) { var app *App var registrationErr error command := NewCommand(CommandSpec[struct{}]{ Name: "inspect", Run: func(Invocation, struct{}) Outcome { registrationErr = test.register(app) return Usage("operation rejected").WithCause(errors.New("operation rejected")) }, }) var err error app, err = New(Identity{Name: "Example"}, command, Alias("shortcut", "inspect")) if err != nil { t.Fatal(err) } var diagnostics bytes.Buffer outcome := app.Execute(context.Background(), []string{"shortcut"}, Streams{Out: io.Discard, Err: &diagnostics}) if outcome.Kind != OutcomeUsage { t.Fatalf("outcome kind = %v, want usage", outcome.Kind) } if (registrationErr != nil) != test.wantErr { t.Fatalf("registration error = %v, want error %t", registrationErr, test.wantErr) } for _, fragment := range []string{"operation rejected", "Usage: Example shortcut", "Alias for: Example inspect"} { if !strings.Contains(diagnostics.String(), fragment) { t.Errorf("diagnostics %q do not contain %q", diagnostics.String(), fragment) } } }) } } func TestUnresolvedRouteUsageHasNoCanonicalAliasAnnotation(t *testing.T) { app := routeTestApp(t) var diagnostics bytes.Buffer outcome := app.Execute(context.Background(), []string{"missing", "route"}, Streams{Out: io.Discard, Err: &diagnostics}) if outcome.Kind != OutcomeUsage { t.Fatalf("outcome kind = %v, want usage", outcome.Kind) } if strings.Contains(diagnostics.String(), "Alias for:") { t.Fatalf("unresolved route diagnostics invented a canonical target: %q", diagnostics.String()) } diagnostics.Reset() outcome = app.Execute(context.Background(), []string{"r", "full", "--unknown"}, Streams{Out: io.Discard, Err: &diagnostics}) if outcome.Kind != OutcomeUsage { t.Fatalf("unknown flag outcome kind = %v, want usage", outcome.Kind) } for _, fragment := range []string{"unknown flag", "Usage: Example r full", "Alias for: Example inspect"} { if !strings.Contains(diagnostics.String(), fragment) { t.Errorf("unknown flag diagnostics %q do not contain %q", diagnostics.String(), fragment) } } } func TestExecuteAppliesGlobalHelpAliasAtApplicationRoot(t *testing.T) { app, err := New(Identity{Name: "Example"}, FlagAlias("--hlep", "--help")) if err != nil { t.Fatal(err) } var output bytes.Buffer outcome := app.Execute(context.Background(), []string{"--hlep"}, Streams{Out: &output, Err: io.Discard}) if outcome.Kind != OutcomeSuccess || !strings.Contains(output.String(), "Usage:") { t.Fatalf("root help alias outcome = %v, output %q", outcome.Kind, output.String()) } } func TestAliasOnlyPrefixProducesDiscoveryRoute(t *testing.T) { app, err := New(Identity{Name: "Example"}, Group("promptkit", "PromptKit", Group("list", "List", simpleRunnable("collections"))), Alias("list collections", "promptkit list collections"), ) if err != nil { t.Fatal(err) } result := resolveEnteredRoute([]string{"list", "--help"}, app.routeSnapshot()) if result.kind != routeDiscovery { t.Fatalf("route kind = %d, want discovery", result.kind) } if got, want := result.discoveryPath.String(), "list"; got != want { t.Fatalf("discovery path = %q, want %q", got, want) } if got, want := result.enteredPath.String(), "list"; got != want { t.Fatalf("entered path = %q, want %q", got, want) } if !reflect.DeepEqual(result.args, []string{"--help"}) { t.Fatalf("discovery args = %q, want [--help]", result.args) } } func TestAliasHelpTargetsGroupsAndDiscoveryViews(t *testing.T) { var inputBuilds, promptRuns int showPrompt := NewCommand(CommandSpec[struct{}]{ Name: "prompt", NewInput: func() struct{} { inputBuilds++ return struct{}{} }, Flags: []Flag[struct{}]{ValueFlag(FlagMeta{Name: "format"}, (*string)(nil), func(value string) (string, error) { return value, nil }, func(*struct{}, Supplied[string]) {}, )}, Run: func(Invocation, struct{}) Outcome { promptRuns++ return Success() }, }) commands := Group("promptkit", "PromptKit", Group("list", "List", simpleRunnable("collections")), Group("show", "Show", showPrompt), ) app, err := New(Identity{Name: "example"}, commands, Alias("list collections", "promptkit list collections"), Alias("promptkit list bundles", "promptkit list collections"), Alias("pk", "promptkit"), Alias("pk list bundles", "promptkit show prompt"), FlagAlias("--hlep", "--help"), FlagAliasFor("promptkit show prompt", "--fmt", "--format"), ) if err != nil { t.Fatal(err) } for _, test := range []struct { name string args []string want []string omit []string }{ { name: "explicit command help resolves entered target", args: []string{"help", "list", "collections"}, want: []string{"Usage:\n example list collections", "Alias for: example promptkit list collections"}, }, { name: "aliased help flag uses entered command path", args: []string{"list", "collections", "--hlep"}, want: []string{"Usage:\n example list collections", "Alias for: example promptkit list collections", "--hlep → --help"}, }, { name: "canonical group shows direct aliases", args: []string{"help", "promptkit", "list"}, want: []string{"Command Aliases:", "bundles → promptkit list collections"}, }, { name: "group shortcut shows more-specific override", args: []string{"pk", "--help"}, want: []string{"Usage:\n example pk [flags]", "Alias for: example promptkit", "list bundles → promptkit show prompt"}, omit: []string{"list bundles → promptkit list collections"}, }, { name: "explicit discovery target", args: []string{"help", "list"}, want: []string{"Usage:\n example list [command]", "Available Aliases:", "collections → promptkit list collections", "--hlep → --help"}, omit: []string{"--fmt"}, }, { name: "discovery help flag", args: []string{"list", "--hlep"}, want: []string{"Usage:\n example list [command]", "collections → promptkit list collections"}, }, { name: "scoped flag alias appears on canonical command help", args: []string{"promptkit", "show", "prompt", "--hlep"}, want: []string{"--fmt → --format"}, }, { name: "group excludes descendant scoped flag aliases", args: []string{"promptkit", "--help"}, want: []string{"Command Aliases:"}, omit: []string{"--fmt → --format"}, }, { name: "root lists explicit command aliases separately", args: []string{"--help"}, want: []string{"Available Commands:", "Command Aliases:", "list collections → promptkit list collections", "pk → promptkit"}, }, } { t.Run(test.name, func(t *testing.T) { var output bytes.Buffer outcome := app.Execute(context.Background(), test.args, Streams{Out: &output, Err: io.Discard}) if outcome.Kind != OutcomeSuccess { t.Fatalf("Execute(%q) = %#v; output %q", test.args, outcome, output.String()) } for _, fragment := range test.want { if !strings.Contains(output.String(), fragment) { t.Errorf("help output %q does not contain %q", output.String(), fragment) } } for _, fragment := range test.omit { if strings.Contains(output.String(), fragment) { t.Errorf("help output %q unexpectedly contains %q", output.String(), fragment) } } }) } var diagnostics bytes.Buffer outcome := app.Execute(context.Background(), []string{"list"}, Streams{Out: io.Discard, Err: &diagnostics}) if outcome.Kind != OutcomeUsage || !strings.Contains(outcome.Cause.Error(), "--help") { t.Fatalf("bare discovery prefix = %#v, diagnostics %q; want usage pointing to list --help", outcome, diagnostics.String()) } if inputBuilds != 0 || promptRuns != 0 { t.Fatalf("help built %d inputs and ran %d commands", inputBuilds, promptRuns) } }