2026-10-19 17:23:32 +00:00
|
|
|
package commandkit_test
|
|
|
|
|
|
|
|
|
|
import (
|
2026-10-19 19:48:57 +00:00
|
|
|
"bytes"
|
|
|
|
|
"context"
|
|
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
|
|
|
|
"io"
|
|
|
|
|
"reflect"
|
|
|
|
|
"strings"
|
2026-10-19 17:23:32 +00:00
|
|
|
"testing"
|
|
|
|
|
|
|
|
|
|
commandkit "gitea.maximumdirect.net/eric/CommandKit"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func TestPublicInstallationAcceptsMixedDeclarations(t *testing.T) {
|
|
|
|
|
command := commandkit.NewCommand(commandkit.CommandSpec[struct{}]{
|
|
|
|
|
Name: "show",
|
|
|
|
|
Summary: "Show a value.",
|
|
|
|
|
Flags: []commandkit.Flag[struct{}]{
|
|
|
|
|
commandkit.ValueFlag(commandkit.FlagMeta{Name: "format"}, (*string)(nil), func(value string) (string, error) { return value, nil }, func(*struct{}, commandkit.Supplied[string]) {}),
|
|
|
|
|
},
|
|
|
|
|
Run: func(commandkit.Invocation, struct{}) commandkit.Outcome { return commandkit.Success() },
|
|
|
|
|
})
|
|
|
|
|
declarations := []commandkit.Declaration{
|
|
|
|
|
command,
|
|
|
|
|
commandkit.Alias("inspect", "show"),
|
|
|
|
|
commandkit.FlagAlias("--fmt", "--format"),
|
|
|
|
|
commandkit.FlagAliasFor("show", "-f", "--format"),
|
|
|
|
|
}
|
|
|
|
|
app, err := commandkit.New(commandkit.Identity{Name: "Example"}, declarations...)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if app == nil {
|
|
|
|
|
t.Fatal("New() returned a nil app")
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-10-19 19:48:57 +00:00
|
|
|
|
|
|
|
|
func TestPublicAliasesPreserveCommandsAcrossDeclarationOrders(t *testing.T) {
|
|
|
|
|
for _, test := range []struct {
|
|
|
|
|
name string
|
|
|
|
|
rulesFirst bool
|
|
|
|
|
}{
|
|
|
|
|
{name: "commands first"},
|
|
|
|
|
{name: "routing rules first", rulesFirst: true},
|
|
|
|
|
} {
|
|
|
|
|
t.Run(test.name, func(t *testing.T) {
|
|
|
|
|
type runInput struct{ words commandkit.Supplied[[]string] }
|
|
|
|
|
type inspectInput struct{ id commandkit.Supplied[string] }
|
|
|
|
|
type showInput struct {
|
|
|
|
|
profile commandkit.Supplied[string]
|
|
|
|
|
format commandkit.Supplied[string]
|
|
|
|
|
}
|
|
|
|
|
var runParses, inspectParses, optionalParses int
|
|
|
|
|
var runValues [][]string
|
|
|
|
|
var inspectValues []string
|
|
|
|
|
var showValues []string
|
|
|
|
|
run := commandkit.NewCommand(commandkit.CommandSpec[runInput]{
|
|
|
|
|
Name: "run",
|
|
|
|
|
Arguments: []commandkit.Argument[runInput]{commandkit.RepeatArgument(commandkit.ArgumentMeta{Name: "words"}, 0, -1,
|
|
|
|
|
func(value string) (string, error) { runParses++; return value, nil },
|
|
|
|
|
func(target *runInput, value commandkit.Supplied[[]string]) { target.words = value },
|
|
|
|
|
)},
|
|
|
|
|
Run: func(_ commandkit.Invocation, value runInput) commandkit.Outcome {
|
|
|
|
|
runValues = append(runValues, append([]string(nil), value.words.Value...))
|
|
|
|
|
return commandkit.Success()
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
inspect := commandkit.NewCommand(commandkit.CommandSpec[inspectInput]{
|
|
|
|
|
Name: "inspect",
|
|
|
|
|
Arguments: []commandkit.Argument[inspectInput]{commandkit.ValueArgument(commandkit.ArgumentMeta{Name: "id"}, true,
|
|
|
|
|
func(value string) (string, error) { inspectParses++; return value, nil },
|
|
|
|
|
func(target *inspectInput, value commandkit.Supplied[string]) { target.id = value },
|
|
|
|
|
)},
|
|
|
|
|
Run: func(_ commandkit.Invocation, value inspectInput) commandkit.Outcome {
|
|
|
|
|
inspectValues = append(inspectValues, value.id.Value)
|
|
|
|
|
return commandkit.Success()
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
show := commandkit.NewCommand(commandkit.CommandSpec[showInput]{
|
|
|
|
|
Name: "show",
|
|
|
|
|
Arguments: []commandkit.Argument[showInput]{commandkit.ValueArgument(commandkit.ArgumentMeta{Name: "profile"}, false,
|
|
|
|
|
func(value string) (string, error) { optionalParses++; return value, nil },
|
|
|
|
|
func(target *showInput, value commandkit.Supplied[string]) { target.profile = value },
|
|
|
|
|
)},
|
|
|
|
|
Flags: []commandkit.Flag[showInput]{commandkit.ValueFlag(commandkit.FlagMeta{Name: "format"}, (*string)(nil),
|
|
|
|
|
func(value string) (string, error) { return value, nil },
|
|
|
|
|
func(target *showInput, value commandkit.Supplied[string]) { target.format = value },
|
|
|
|
|
)},
|
|
|
|
|
Run: func(_ commandkit.Invocation, value showInput) commandkit.Outcome {
|
|
|
|
|
showValues = append(showValues, value.profile.Value+":"+value.format.Value)
|
|
|
|
|
return commandkit.Success()
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
commands := []commandkit.Declaration{run, inspect, show}
|
|
|
|
|
rules := []commandkit.Declaration{
|
|
|
|
|
commandkit.Alias("r", "run"),
|
|
|
|
|
commandkit.Alias("r full", "inspect"),
|
|
|
|
|
commandkit.Alias("inspect-shortcut", "inspect"),
|
|
|
|
|
commandkit.Alias("i", "inspect-shortcut"),
|
|
|
|
|
commandkit.FlagAlias("--fmt", "--format"),
|
|
|
|
|
}
|
|
|
|
|
declarations := append([]commandkit.Declaration(nil), commands...)
|
|
|
|
|
if test.rulesFirst {
|
|
|
|
|
declarations = append(append([]commandkit.Declaration(nil), rules...), commands...)
|
|
|
|
|
} else {
|
|
|
|
|
declarations = append(declarations, rules...)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
app, err := commandkit.New(commandkit.Identity{Name: "Example"}, declarations...)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if runParses != 0 || inspectParses != 0 || optionalParses != 0 {
|
|
|
|
|
t.Fatalf("installation invoked operand converters: %d/%d/%d", runParses, inspectParses, optionalParses)
|
|
|
|
|
}
|
|
|
|
|
for _, args := range [][]string{{"run", "full"}, {"r", "full", "item-1"}, {"i", "item-2"}, {"show", "profile", "--fmt=json"}} {
|
|
|
|
|
if outcome := app.Execute(context.Background(), args, commandkit.Streams{Out: io.Discard, Err: io.Discard}); outcome.Kind != commandkit.OutcomeSuccess {
|
|
|
|
|
t.Fatalf("Execute(%q) = %#v, want success", args, outcome)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if !reflect.DeepEqual(runValues, [][]string{{"full"}}) {
|
|
|
|
|
t.Fatalf("canonical run operands = %#v, want [[full]]", runValues)
|
|
|
|
|
}
|
|
|
|
|
if !reflect.DeepEqual(inspectValues, []string{"item-1", "item-2"}) {
|
|
|
|
|
t.Fatalf("routed required operands = %#v", inspectValues)
|
|
|
|
|
}
|
|
|
|
|
if !reflect.DeepEqual(showValues, []string{"profile:json"}) || optionalParses != 1 {
|
|
|
|
|
t.Fatalf("optional operand/flag values = %#v, converter calls %d", showValues, optionalParses)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
parsesBeforeReuse := [3]int{runParses, inspectParses, optionalParses}
|
|
|
|
|
secondApp, err := commandkit.New(commandkit.Identity{Name: "Second"}, declarations...)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("reusing declarations: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if got := [3]int{runParses, inspectParses, optionalParses}; got != parsesBeforeReuse {
|
|
|
|
|
t.Fatalf("reinstalling declarations invoked operand converters: %v -> %v", parsesBeforeReuse, got)
|
|
|
|
|
}
|
|
|
|
|
if outcome := secondApp.Execute(context.Background(), []string{"run", "again"}, commandkit.Streams{Out: io.Discard, Err: io.Discard}); outcome.Kind != commandkit.OutcomeSuccess {
|
|
|
|
|
t.Fatalf("reused declaration execution = %#v", outcome)
|
|
|
|
|
}
|
|
|
|
|
if !reflect.DeepEqual(runValues, [][]string{{"full"}, {"again"}}) {
|
|
|
|
|
t.Fatalf("reused command declaration values = %#v", runValues)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestPublicRootGroupRegistrationAndFailedBatchRecovery(t *testing.T) {
|
|
|
|
|
type showInput struct{ format commandkit.Supplied[string] }
|
|
|
|
|
var formatParses, lateRuns int
|
|
|
|
|
var formats []string
|
|
|
|
|
show := commandkit.NewCommand(commandkit.CommandSpec[showInput]{
|
|
|
|
|
Name: "show",
|
|
|
|
|
Flags: []commandkit.Flag[showInput]{commandkit.ValueFlag(commandkit.FlagMeta{Name: "format"}, (*string)(nil),
|
|
|
|
|
func(value string) (string, error) { formatParses++; return value, nil },
|
|
|
|
|
func(target *showInput, value commandkit.Supplied[string]) { target.format = value },
|
|
|
|
|
)},
|
|
|
|
|
Run: func(_ commandkit.Invocation, value showInput) commandkit.Outcome {
|
|
|
|
|
formats = append(formats, value.format.Value)
|
|
|
|
|
return commandkit.Success()
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
family := commandkit.RootGroup("config", "Configuration", show)
|
|
|
|
|
constructionFailure := errors.New("family construction failed")
|
|
|
|
|
if _, err := commandkit.New(commandkit.Identity{Name: "Broken"}, commandkit.WithError(family, fmt.Errorf("adapter: %w", constructionFailure))); !errors.Is(err, constructionFailure) {
|
|
|
|
|
t.Fatalf("New() error = %v, want retained family construction failure", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
app, err := commandkit.New(commandkit.Identity{Name: "Example"})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
var before bytes.Buffer
|
|
|
|
|
app.Execute(context.Background(), []string{"--help"}, commandkit.Streams{Out: &before, Err: io.Discard})
|
|
|
|
|
if strings.Contains(before.String(), "config") {
|
|
|
|
|
t.Fatalf("root help showed an unregistered family: %q", before.String())
|
|
|
|
|
}
|
|
|
|
|
if err := app.Register(family,
|
|
|
|
|
commandkit.Alias("cfg-show", "config show"),
|
|
|
|
|
commandkit.Alias("cs", "cfg-show"),
|
|
|
|
|
commandkit.FlagAlias("--fmt", "--format"),
|
|
|
|
|
); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if formatParses != 0 {
|
|
|
|
|
t.Fatalf("registration invoked flag converter %d times", formatParses)
|
|
|
|
|
}
|
|
|
|
|
var after bytes.Buffer
|
|
|
|
|
app.Execute(context.Background(), []string{"--help"}, commandkit.Streams{Out: &after, Err: io.Discard})
|
|
|
|
|
for _, fragment := range []string{"config", "Aliases:", "cfg-show", "cs"} {
|
|
|
|
|
if !strings.Contains(after.String(), fragment) {
|
|
|
|
|
t.Errorf("updated root help %q does not contain %q", after.String(), fragment)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if outcome := app.Execute(context.Background(), []string{"cs", "--fmt=yaml"}, commandkit.Streams{Out: io.Discard, Err: io.Discard}); outcome.Kind != commandkit.OutcomeSuccess {
|
|
|
|
|
t.Fatalf("registered alias execution = %#v", outcome)
|
|
|
|
|
}
|
|
|
|
|
if !reflect.DeepEqual(formats, []string{"yaml"}) || formatParses != 1 {
|
|
|
|
|
t.Fatalf("registered family values = %#v, converter calls %d", formats, formatParses)
|
|
|
|
|
}
|
|
|
|
|
if err := app.Register(commandkit.NewCommand(commandkit.CommandSpec[struct{}]{Name: "cfg-show", Run: func(commandkit.Invocation, struct{}) commandkit.Outcome { return commandkit.Success() }})); err == nil {
|
|
|
|
|
t.Fatal("registration accepted a canonical command that conflicts with an existing alias")
|
|
|
|
|
}
|
|
|
|
|
var afterConflict bytes.Buffer
|
|
|
|
|
app.Execute(context.Background(), []string{"--help"}, commandkit.Streams{Out: &afterConflict, Err: io.Discard})
|
|
|
|
|
if !strings.Contains(afterConflict.String(), "cfg-show") {
|
|
|
|
|
t.Fatalf("failed relationship validation changed registered aliases: %q", afterConflict.String())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
late := commandkit.NewCommand(commandkit.CommandSpec[struct{}]{Name: "late", Run: func(commandkit.Invocation, struct{}) commandkit.Outcome {
|
|
|
|
|
lateRuns++
|
|
|
|
|
return commandkit.Success()
|
|
|
|
|
}})
|
|
|
|
|
registrationFailure := errors.New("retained registration failure")
|
|
|
|
|
err = app.Register(commandkit.WithError(late, fmt.Errorf("family: %w", registrationFailure)))
|
|
|
|
|
if !errors.Is(err, registrationFailure) {
|
|
|
|
|
t.Fatalf("Register() error = %v, want retained failure", err)
|
|
|
|
|
}
|
|
|
|
|
if err := app.Register(late, commandkit.Alias("late-shortcut", "missing")); err == nil {
|
|
|
|
|
t.Fatal("registration accepted a batch whose alias target is absent")
|
|
|
|
|
}
|
|
|
|
|
var afterFailure bytes.Buffer
|
|
|
|
|
app.Execute(context.Background(), []string{"--help"}, commandkit.Streams{Out: &afterFailure, Err: io.Discard})
|
|
|
|
|
if strings.Contains(afterFailure.String(), "late-shortcut") || strings.Contains(afterFailure.String(), " late ") {
|
|
|
|
|
t.Fatalf("failed batch changed root help: %q", afterFailure.String())
|
|
|
|
|
}
|
|
|
|
|
if err := app.Register(late, commandkit.Alias("late-shortcut", "late")); err != nil {
|
|
|
|
|
t.Fatalf("registration after failed batch: %v", err)
|
|
|
|
|
}
|
|
|
|
|
var recovered bytes.Buffer
|
|
|
|
|
app.Execute(context.Background(), []string{"--help"}, commandkit.Streams{Out: &recovered, Err: io.Discard})
|
|
|
|
|
if !strings.Contains(recovered.String(), "late-shortcut") {
|
|
|
|
|
t.Fatalf("recovered root help omitted alias: %q", recovered.String())
|
|
|
|
|
}
|
|
|
|
|
if outcome := app.Execute(context.Background(), []string{"late-shortcut"}, commandkit.Streams{Out: io.Discard, Err: io.Discard}); outcome.Kind != commandkit.OutcomeSuccess || lateRuns != 1 {
|
|
|
|
|
t.Fatalf("recovered alias execution = %#v, runs %d", outcome, lateRuns)
|
|
|
|
|
}
|
|
|
|
|
}
|