package commandkit import ( "bytes" "context" "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 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 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{"list", "--help"}) { t.Fatalf("discovery args = %q, want [list --help]", result.args) } }