package commandkit import ( "bytes" "context" "errors" "fmt" "io" "reflect" "strings" "testing" ) 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(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) } }