Capture immutable command declarations

This commit is contained in:
2026-10-11 01:15:13 +00:00
parent 476e98a463
commit de9bfdcc4c
16 changed files with 368 additions and 474 deletions

View File

@@ -1,46 +1,29 @@
package commandkit
import (
"errors"
"testing"
)
type registryInput struct {
value string
}
type registryInput struct{ value string }
func TestNewAndRegisterRetainCallbacksWithoutCallingThem(t *testing.T) {
func TestCapturedDeclarationsRetainCallbacksWithoutCallingThem(t *testing.T) {
var factoryCalls, prepareCalls, setterCalls, checkCalls, runCalls int
flag := Flag[registryInput]{binding: flagBinding[registryInput]{
meta: FlagMeta{Name: "mode"},
valid: true,
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()
},
command := 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)
}
group := Group("Tools", "tools", command)
app, err := New(Identity{Name: "Example"}, group)
if err != nil {
t.Fatal(err)
@@ -48,11 +31,10 @@ func TestNewAndRegisterRetainCallbacksWithoutCallingThem(t *testing.T) {
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)
t.Fatalf("construction invoked 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() {
if !app.commands["Tools"].command.record.children[0].record.payload.hasRun() {
t.Fatal("registered command lost its handler")
}
}
@@ -66,10 +48,7 @@ func TestAppsHaveIndependentRegistries(t *testing.T) {
if err != nil {
t.Fatal(err)
}
command, err := NewCommand(CommandSpec[struct{}]{Name: "custom"})
if err != nil {
t.Fatal(err)
}
command := NewCommand(CommandSpec[struct{}]{Name: "custom"})
if err := first.Register(command); err != nil {
t.Fatal(err)
}
@@ -81,152 +60,126 @@ func TestAppsHaveIndependentRegistries(t *testing.T) {
}
}
func TestCommandAndGroupNames(t *testing.T) {
func TestDeclarationIssuesAreReportedAtInstallation(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)
app, err := New(Identity{Name: "Example"})
if err != nil {
t.Fatal(err)
}
if err := app.Register(NewCommand(CommandSpec[struct{}]{Name: name})); err == nil {
t.Errorf("installation 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(NewCommand(CommandSpec[struct{}]{Name: "CamelCase"})); err != nil {
t.Fatalf("mixed-case name rejected: %v", err)
}
if err := app.Register(existing); err != nil {
t.Fatal(err)
duplicate := Group("parent", "", NewCommand(CommandSpec[struct{}]{Name: "repeat"}), NewCommand(CommandSpec[struct{}]{Name: "repeat"}))
if err := app.Register(duplicate); err == nil {
t.Fatal("installation accepted duplicate sibling names")
}
before := app.commands["existing"]
added, err := NewCommand(CommandSpec[struct{}]{Name: "added"})
if err != nil {
t.Fatal(err)
if err := app.Register(Group("parent", "", Command{})); err == nil {
t.Fatal("installation accepted a zero child handle")
}
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")
}
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")
}
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)
}
if err := app.Register(Command{}); err == nil {
t.Fatal("installation accepted a zero command handle")
}
}
func TestNewRejectsEmptyIdentityAndInvalidBindingMetadata(t *testing.T) {
func TestRunnableCommandCapturesChildrenAndLeavesOriginalUnchanged(t *testing.T) {
child := NewCommand(CommandSpec[struct{}]{Name: "child"})
children := []Command{child}
parent := NewCommand(CommandSpec[struct{}]{Name: "parent", Children: children, Run: func(Invocation, struct{}) Outcome { return Success() }})
children[0] = NewCommand(CommandSpec[struct{}]{Name: "changed"})
if parent.record.payload == nil || !parent.record.payload.hasRun() {
t.Fatal("runnable parent lost its handler")
}
if len(parent.record.children) != 1 || parent.record.children[0].record.name != "child" {
t.Fatalf("captured children = %#v", parent.record.children)
}
app, err := New(Identity{Name: "Example"}, parent)
if err != nil {
t.Fatalf("runnable parent with children rejected: %v", err)
}
if _, ok := app.commandAt([]string{"parent", "child"}); !ok {
t.Fatal("child declaration was not installed")
}
}
func TestGroupCapturesChildrenAndSkipsNestedAbsence(t *testing.T) {
children := []Command{NewCommand(CommandSpec[struct{}]{Name: "visible"}), NoCommand()}
group := Group("parent", "", children...)
children[0] = NewCommand(CommandSpec[struct{}]{Name: "changed"})
if len(group.record.children) != 2 || group.record.children[0].record.name != "visible" {
t.Fatalf("group did not capture child handles: %#v", group.record.children)
}
app, err := New(Identity{Name: "Example"}, group)
if err != nil {
t.Fatal(err)
}
if _, ok := app.commandAt([]string{"parent", "visible"}); !ok {
t.Fatal("captured child missing")
}
}
func TestRootGroupRetainsPlacementMetadata(t *testing.T) {
root := RootGroup("config", "Configuration.")
if !root.record.rootOnly || root.record.absent || root.record.name != "config" {
t.Fatalf("root group metadata = %#v", root.record)
}
copy := root
if copy.record != root.record {
t.Fatal("copied handle did not retain captured record")
}
}
func TestWithErrorCopiesDeclarationAndRetainsWrappedCauses(t *testing.T) {
first := errors.New("first construction issue")
second := errors.New("second construction issue")
base := NewCommand(CommandSpec[struct{}]{Name: "run"})
withFirst := WithError(base, first)
withBoth := WithError(withFirst, errors.Join(errors.New("wrapper"), second))
if len(base.record.issues) != 0 {
t.Fatal("WithError mutated its source declaration")
}
if len(withFirst.record.issues) != 1 || len(withBoth.record.issues) != 2 {
t.Fatal("WithError did not retain prior causes")
}
app, err := New(Identity{Name: "Example"})
if err != nil {
t.Fatal(err)
}
err = app.Register(withBoth)
if !errors.Is(err, first) || !errors.Is(err, second) {
t.Fatalf("installation error %v did not preserve causes", err)
}
if err := app.Register(WithError(NoCommand(), first)); !errors.Is(err, first) {
t.Fatalf("error on absence = %v", err)
}
if err := app.Register(NoCommand()); err != nil {
t.Fatalf("plain absence was not skipped: %v", err)
}
if err := app.Register(RootGroup("empty", "")); err != nil {
t.Fatalf("real empty root group rejected: %v", err)
}
}
func TestNewRejectsEmptyIdentity(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", "")},
{validFlag("SomeFlag", "")},
{validFlag("some_flag", "")},
{validFlag("some--flag", "")},
{{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 := []Flag[registryInput]{{binding: flagBinding[registryInput]{meta: FlagMeta{Name: "mode"}, valid: true, prepare: func([]string) (func(*registryInput), error) { return func(*registryInput) {}, nil }}}}
command := NewCommand(CommandSpec[registryInput]{Name: "run", Flags: flags})
flags[0].binding.meta.Name = "changed"
payload := command.payload.(typedCommand[registryInput])
payload := command.record.payload.(typedCommand[registryInput])
if got := payload.spec.Flags[0].binding.meta.Name; got != "mode" {
t.Fatalf("stored flag name = %q, want %q", got, "mode")
t.Fatalf("stored flag name = %q, want mode", got)
}
}