2026-10-07 13:16:52 +00:00
|
|
|
package commandkit
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"testing"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type registryInput struct {
|
|
|
|
|
value string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestNewAndRegisterRetainCallbacksWithoutCallingThem(t *testing.T) {
|
|
|
|
|
var factoryCalls, prepareCalls, setterCalls, checkCalls, runCalls int
|
|
|
|
|
flag := Flag[registryInput]{binding: flagBinding[registryInput]{
|
|
|
|
|
meta: FlagMeta{Name: "mode"},
|
|
|
|
|
valid: true,
|
|
|
|
|
prepare: func([]string) (func(*registryInput), error) {
|
|
|
|
|
prepareCalls++
|
|
|
|
|
return func(*registryInput) { setterCalls++ }, nil
|
|
|
|
|
},
|
|
|
|
|
}}
|
|
|
|
|
command, err := NewCommand(CommandSpec[registryInput]{
|
|
|
|
|
Name: "Inspect",
|
|
|
|
|
NewInput: func() registryInput {
|
|
|
|
|
factoryCalls++
|
|
|
|
|
return registryInput{}
|
|
|
|
|
},
|
|
|
|
|
Flags: []Flag[registryInput]{flag},
|
|
|
|
|
CheckInput: func(registryInput) error {
|
|
|
|
|
checkCalls++
|
|
|
|
|
return nil
|
|
|
|
|
},
|
|
|
|
|
Run: func(Invocation, registryInput) Outcome {
|
|
|
|
|
runCalls++
|
|
|
|
|
return Success()
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
group, err := Group("Tools", "tools", command)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
app, err := New(Identity{Name: "Example"}, group)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if err := app.Register(); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if factoryCalls+prepareCalls+setterCalls+checkCalls+runCalls != 0 {
|
|
|
|
|
t.Fatalf("construction invoked a callback: factory=%d prepare=%d setter=%d check=%d run=%d", factoryCalls, prepareCalls, setterCalls, checkCalls, runCalls)
|
|
|
|
|
}
|
|
|
|
|
if !app.commands["Tools"].command.children[0].payload.hasRun() {
|
|
|
|
|
t.Fatal("registered command lost its handler")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestAppsHaveIndependentRegistries(t *testing.T) {
|
|
|
|
|
first, err := New(Identity{Name: "First"})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
second, err := New(Identity{Name: "Second"})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
command, err := NewCommand(CommandSpec[struct{}]{Name: "custom"})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if err := first.Register(command); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if _, exists := second.commands["custom"]; exists {
|
|
|
|
|
t.Fatal("registration leaked to another app")
|
|
|
|
|
}
|
|
|
|
|
if !first.commands["help"].reserved || !first.commands["version"].reserved {
|
|
|
|
|
t.Fatal("New did not reserve generated root commands")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestCommandAndGroupNames(t *testing.T) {
|
|
|
|
|
for _, name := range []string{"", "--", "-option", "two words", " leading", "trailing "} {
|
|
|
|
|
if _, err := NewCommand(CommandSpec[struct{}]{Name: name}); err == nil {
|
|
|
|
|
t.Errorf("NewCommand accepted invalid name %q", name)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if _, err := NewCommand(CommandSpec[struct{}]{Name: "CamelCase"}); err != nil {
|
|
|
|
|
t.Fatalf("NewCommand rejected a custom mixed-case name: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
first, err := NewCommand(CommandSpec[struct{}]{Name: "repeat"})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
second, err := NewCommand(CommandSpec[struct{}]{Name: "repeat"})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if _, err := Group("parent", "", first, second); err == nil {
|
|
|
|
|
t.Fatal("Group accepted duplicate sibling paths")
|
|
|
|
|
}
|
|
|
|
|
if _, err := Group("parent", "", Command{}); err == nil {
|
|
|
|
|
t.Fatal("Group accepted an invalid child declaration")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestWithChildrenRetainsRunnableParent(t *testing.T) {
|
|
|
|
|
parent, err := NewCommand(CommandSpec[struct{}]{Name: "parent", Run: func(Invocation, struct{}) Outcome {
|
|
|
|
|
return Success()
|
|
|
|
|
}})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
child, err := NewCommand(CommandSpec[struct{}]{Name: "child"})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
composed, err := WithChildren(parent, child)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if composed.payload == nil || !composed.payload.hasRun() {
|
|
|
|
|
t.Fatal("WithChildren did not retain the runnable parent")
|
|
|
|
|
}
|
|
|
|
|
if len(composed.children) != 1 || composed.children[0].name != "child" {
|
|
|
|
|
t.Fatalf("children = %#v, want relative child declaration", composed.children)
|
|
|
|
|
}
|
|
|
|
|
if len(parent.children) != 0 {
|
|
|
|
|
t.Fatal("WithChildren changed the original parent declaration")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestRegisterBatchIsAtomicAndRejectsReservedPaths(t *testing.T) {
|
|
|
|
|
app, err := New(Identity{Name: "Example"})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
existing, err := NewCommand(CommandSpec[struct{}]{Name: "existing"})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if err := app.Register(existing); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
before := app.commands["existing"]
|
|
|
|
|
added, err := NewCommand(CommandSpec[struct{}]{Name: "added"})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if err := app.Register(added, existing); err == nil {
|
|
|
|
|
t.Fatal("Register accepted a duplicate path")
|
|
|
|
|
}
|
|
|
|
|
if app.commands["existing"] != before {
|
|
|
|
|
t.Fatal("failed batch changed an existing registration")
|
|
|
|
|
}
|
|
|
|
|
if _, exists := app.commands["added"]; exists {
|
|
|
|
|
t.Fatal("failed batch attached a partial registration")
|
|
|
|
|
}
|
2026-10-09 01:32:36 +00:00
|
|
|
if err := app.Register(added, added); err == nil {
|
|
|
|
|
t.Fatal("Register accepted a duplicate within a batch")
|
|
|
|
|
}
|
|
|
|
|
if _, exists := app.commands["added"]; exists {
|
|
|
|
|
t.Fatal("failed duplicate batch attached a registration")
|
|
|
|
|
}
|
2026-10-07 13:16:52 +00:00
|
|
|
for _, name := range []string{"help", "version"} {
|
|
|
|
|
reserved, err := NewCommand(CommandSpec[struct{}]{Name: name})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if err := app.Register(reserved); err == nil {
|
|
|
|
|
t.Errorf("Register replaced reserved command %q", name)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestNewRejectsEmptyIdentityAndInvalidBindingMetadata(t *testing.T) {
|
|
|
|
|
if _, err := New(Identity{}); err == nil {
|
|
|
|
|
t.Fatal("New accepted an empty identity name")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
validFlag := func(name, short string) Flag[registryInput] {
|
|
|
|
|
return Flag[registryInput]{binding: flagBinding[registryInput]{
|
|
|
|
|
meta: FlagMeta{Name: name, Short: short},
|
|
|
|
|
valid: true,
|
|
|
|
|
prepare: func([]string) (func(*registryInput), error) { return func(*registryInput) {}, nil },
|
|
|
|
|
}}
|
|
|
|
|
}
|
|
|
|
|
for _, flags := range [][]Flag[registryInput]{
|
|
|
|
|
{validFlag("one", "x"), validFlag("one", "y")},
|
|
|
|
|
{validFlag("one", "x"), validFlag("two", "x")},
|
|
|
|
|
{validFlag("help", "")},
|
2026-10-07 13:22:57 +00:00
|
|
|
{validFlag("SomeFlag", "")},
|
|
|
|
|
{validFlag("some_flag", "")},
|
|
|
|
|
{validFlag("some--flag", "")},
|
2026-10-07 13:16:52 +00:00
|
|
|
{{binding: flagBinding[registryInput]{meta: FlagMeta{Name: "missing"}}}},
|
|
|
|
|
} {
|
|
|
|
|
if _, err := NewCommand(CommandSpec[registryInput]{Name: "flags", Flags: flags}); err == nil {
|
|
|
|
|
t.Errorf("NewCommand accepted invalid flag metadata: %#v", flags)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
badArgument := Argument[registryInput]{binding: argumentBinding[registryInput]{
|
|
|
|
|
meta: ArgumentMeta{Name: "item"},
|
|
|
|
|
min: 2,
|
|
|
|
|
max: 1,
|
|
|
|
|
valid: true,
|
|
|
|
|
prepare: func([]string) (func(*registryInput), error) { return func(*registryInput) {}, nil },
|
|
|
|
|
}}
|
|
|
|
|
if _, err := NewCommand(CommandSpec[registryInput]{Name: "args", Arguments: []Argument[registryInput]{badArgument}}); err == nil {
|
|
|
|
|
t.Fatal("NewCommand accepted invalid argument bounds")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestNewCommandCopiesBindingMetadata(t *testing.T) {
|
|
|
|
|
flags := []Flag[registryInput]{{binding: flagBinding[registryInput]{
|
|
|
|
|
meta: FlagMeta{Name: "mode"},
|
|
|
|
|
valid: true,
|
|
|
|
|
prepare: func([]string) (func(*registryInput), error) { return func(*registryInput) {}, nil },
|
|
|
|
|
}}}
|
|
|
|
|
command, err := NewCommand(CommandSpec[registryInput]{Name: "run", Flags: flags})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
flags[0].binding.meta.Name = "changed"
|
|
|
|
|
payload := command.payload.(typedCommand[registryInput])
|
|
|
|
|
if got := payload.spec.Flags[0].binding.meta.Name; got != "mode" {
|
|
|
|
|
t.Fatalf("stored flag name = %q, want %q", got, "mode")
|
|
|
|
|
}
|
|
|
|
|
}
|