Keep command routing independent of operand counts

This commit is contained in:
2026-10-20 00:22:25 +00:00
parent ccf8acd861
commit 7dad20680e
5 changed files with 110 additions and 18 deletions

View File

@@ -11,6 +11,89 @@ import (
"testing"
)
func TestCommandContextSurvivesIncompleteAndInvalidOperandCounts(t *testing.T) {
for _, spelling := range []string{"run", "r"} {
for _, test := range []struct {
name string
args []string
help bool
usage bool
}{
{name: "interspersed alias", args: []string{"first", "--fmt=yaml", "second"}},
{name: "interspersed canonical flag", args: []string{"first", "--format=yaml", "second"}},
{name: "partial help", args: []string{"first", "--help"}, help: true},
{name: "partial help with flag alias", args: []string{"first", "--fmt=yaml", "--help"}, help: true},
{name: "excess operands", args: []string{"first", "second", "third", "--fmt=yaml"}, usage: true},
{name: "missing operands", args: []string{"first", "--fmt=yaml"}, usage: true},
} {
t.Run(spelling+"/"+test.name, func(t *testing.T) {
type input struct {
operands []string
format Supplied[string]
}
built, ran := 0, false
command := NewCommand(CommandSpec[input]{
Name: "run", NewInput: func() input { built++; return input{} },
Flags: []Flag[input]{ValueFlag(FlagMeta{Name: "format"}, (*string)(nil),
func(value string) (string, error) { return value, nil },
func(target *input, value Supplied[string]) { target.format = value })},
Arguments: []Argument[input]{RepeatArgument(ArgumentMeta{Name: "ARG"}, 2, 2,
func(value string) (string, error) { return value, nil },
func(target *input, value Supplied[[]string]) { target.operands = value.Value })},
Run: func(inv Invocation, value input) Outcome {
ran = true
if inv.EnteredPath() != spelling || inv.CanonicalPath() != "run" {
t.Errorf("invocation paths = %q / %q", inv.EnteredPath(), inv.CanonicalPath())
}
if !reflect.DeepEqual(value.operands, []string{"first", "second"}) || !value.format.Set || value.format.Value != "yaml" {
t.Errorf("input = %#v", value)
}
return Success()
},
})
app, err := New(Identity{Name: "Demo"}, command, Alias("r", "run"), FlagAliasFor("run", "--fmt", "--format"))
if err != nil {
t.Fatal(err)
}
var output, diagnostics bytes.Buffer
outcome := app.Execute(context.Background(), append([]string{spelling}, test.args...), Streams{Out: &output, Err: &diagnostics})
if test.usage {
if outcome.Kind != OutcomeUsage || strings.Contains(diagnostics.String(), "unknown flag") || !strings.Contains(diagnostics.String(), "Demo "+spelling+" ARG") {
t.Fatalf("invalid arity outcome = %v, diagnostics %q", outcome.Kind, diagnostics.String())
}
} else if outcome.Kind != OutcomeSuccess {
t.Fatalf("outcome = %#v, diagnostics %q", outcome, diagnostics.String())
}
if test.help && (!strings.Contains(output.String(), "Demo "+spelling+" ARG") || !strings.Contains(output.String(), "--fmt → --format")) {
t.Errorf("command help = %q", output.String())
}
if test.help || test.usage {
if built != 0 || ran {
t.Fatal("help or invalid arity initialized input or ran a handler")
}
} else if built != 1 || !ran {
t.Fatal("valid invocation did not run once")
}
})
}
}
}
func TestRunnableCommandCanHaveAliasOnlyDiscoveryPrefixes(t *testing.T) {
ran := false
app, err := New(Identity{Name: "Demo"},
NewCommand(CommandSpec[struct{}]{Name: "box", Run: func(Invocation, struct{}) Outcome { ran = true; return Success() }}),
simpleRunnable("inspect"), Alias("box admin pick", "inspect"))
if err != nil {
t.Fatal(err)
}
var output bytes.Buffer
outcome := app.Execute(context.Background(), []string{"box", "admin", "--help"}, Streams{Out: &output, Err: io.Discard})
if outcome.Kind != OutcomeSuccess || !strings.Contains(output.String(), "pick → inspect") || ran {
t.Fatalf("discovery outcome = %v, help %q, ran %t", outcome.Kind, output.String(), ran)
}
}
func routeTestApp(t *testing.T, extra ...Declaration) *App {
t.Helper()
collections := simpleRunnable("collections")