Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
169 changes: 169 additions & 0 deletions cli/commands/unknown_command_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
package commands

import (
"strings"
"testing"

"miren.dev/mflags"
"miren.dev/runtime/pkg/labs"
)

// dispatchErr builds the full dispatcher and returns the error Execute produces,
// so that tests can assert on what the user sees when a command is mistyped.
func dispatchErr(t *testing.T, args ...string) error {
t.Helper()

d := mflags.NewDispatcher("miren")
RegisterAll(d)

return d.Execute(args)
}

// TestUnknownCommandSuggests guards MIR-1823: a mistyped command must name the
// word that went wrong and point at the real one, rather than reporting a flag
// parsing failure.
func TestUnknownCommandSuggests(t *testing.T) {
labs.EnableAll()

cases := []struct {
args []string
want []string
notWant []string
}{
{
args: []string{"depoy"},
want: []string{`unknown command "depoy"`, "Did you mean?", "deploy", "miren --help"},
notWant: []string{"error parsing flags", "["},
},
{
// The old message reported "error parsing flags: unexpected
// arguments: [lst]" here.
args: []string{"app", "lst"},
want: []string{`unknown command "lst" for "miren app"`, "Did you mean?", "list", "miren app --help"},
notWant: []string{"error parsing flags", "["},
},
{
// Sections tolerate unknown flags, so this used to print help and
// exit 0 with no sign that anything was wrong.
args: []string{"runner", "upgrde"},
want: []string{`unknown command "upgrde" for "miren runner"`, "upgrade"},
notWant: []string{"error parsing flags"},
},
{
// Nothing is close to this, so no guess should be offered.
args: []string{"zzzznotacommand"},
want: []string{`unknown command "zzzznotacommand"`},
notWant: []string{"Did you mean?", "error parsing flags"},
},
{
// "version" has no sub-commands, so there is nothing to suggest —
// but the Go slice syntax and the flag-parsing prefix are still gone.
args: []string{"version", "foo"},
want: []string{`unexpected argument "foo"`},
notWant: []string{"error parsing flags", "[foo]"},
},
}

for _, c := range cases {
t.Run(strings.Join(c.args, "_"), func(t *testing.T) {
err := dispatchErr(t, c.args...)
if err == nil {
t.Fatalf("Execute(%v) returned no error", c.args)
}

got := err.Error()
for _, want := range c.want {
if !strings.Contains(got, want) {
t.Errorf("expected %q in error, got:\n%s", want, got)
}
}
for _, notWant := range c.notWant {
if strings.Contains(got, notWant) {
t.Errorf("did not expect %q in error, got:\n%s", notWant, got)
}
}
})
}
}

// TestUnknownFlagSuggests covers the same treatment for a mistyped flag name.
func TestUnknownFlagSuggests(t *testing.T) {
labs.EnableAll()

cases := []struct {
args []string
want []string
notWant []string
}{
{
args: []string{"deploy", "--aap", "x"},
want: []string{"unknown flag: --aap", "Did you mean?", "--app"},
notWant: []string{"error parsing flags"},
},
{
args: []string{"app", "list", "--forma", "json"},
want: []string{"unknown flag: --forma", "--format"},
notWant: []string{"error parsing flags"},
},
{
// A single letter is too little to guess from.
args: []string{"app", "list", "-Q"},
want: []string{"unknown flag: -Q"},
notWant: []string{"Did you mean?"},
},
{
// No command word was typed at all, so there is nothing to call an
// unknown command — name the flag instead.
args: []string{"--bogus"},
want: []string{"unknown flag: --bogus"},
notWant: []string{`unknown command ""`},
},
}

for _, c := range cases {
t.Run(strings.Join(c.args, "_"), func(t *testing.T) {
err := dispatchErr(t, c.args...)
if err == nil {
t.Fatalf("Execute(%v) returned no error", c.args)
}

got := err.Error()
for _, want := range c.want {
if !strings.Contains(got, want) {
t.Errorf("expected %q in error, got:\n%s", want, got)
}
}
for _, notWant := range c.notWant {
if strings.Contains(got, notWant) {
t.Errorf("did not expect %q in error, got:\n%s", notWant, got)
}
}
})
}
}

// TestKnownCommandsAreNotMistakenForTypos pins the inputs the new check must
// leave alone: help keywords, pass-through arguments, and global flags that
// take a value.
func TestKnownCommandsAreNotMistakenForTypos(t *testing.T) {
labs.EnableAll()

cases := [][]string{
{"app", "--help"},
{"app", "help"},
{"runner", "help"},
{"help", "runner"},
{"-C", "prod", "auth", "help"},
{"app", "run", "echo", "hello"},
}

for _, args := range cases {
t.Run(strings.Join(args, "_"), func(t *testing.T) {
if err := dispatchErr(t, args...); err != nil {
if strings.Contains(err.Error(), "unknown command") {
t.Errorf("Execute(%v) wrongly reported a typo: %v", args, err)
}
}
})
}
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ require (
gopkg.in/yaml.v3 v3.0.1
k8s.io/klog/v2 v2.130.1
miren.dev/lbd v0.0.0-20260224020427-8914d8db2233
miren.dev/mflags v0.0.0-20260910225849-7704913d5c9c
miren.dev/mflags v0.0.0-20260913023833-b11db8e63177
modernc.org/sqlite v1.45.0
sigs.k8s.io/knftables v0.0.21
)
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1955,8 +1955,8 @@ k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
miren.dev/lbd v0.0.0-20260224020427-8914d8db2233 h1:9DxH7Dhnmu7hn1OA2JC5fHpLuVgAqBySws9GLNssLl4=
miren.dev/lbd v0.0.0-20260224020427-8914d8db2233/go.mod h1:+x9fy2p45csBnGUJdqxCUmzlUTCipoVDbv6zIapTgDA=
miren.dev/mflags v0.0.0-20260910225849-7704913d5c9c h1:x4XxiRPIEh64GV1DzDaxGoiMqj+9qYCOnFBW2h+L3Vs=
miren.dev/mflags v0.0.0-20260910225849-7704913d5c9c/go.mod h1:G1eQ/upWVdO6BGT6dlh5Yqjt+9ncH5RUAKX6UKi1F9Q=
miren.dev/mflags v0.0.0-20260913023833-b11db8e63177 h1:7w7JpoGLuFwv3WQHDeYQHyruWA6y+SLNuZIO4LOZggk=
miren.dev/mflags v0.0.0-20260913023833-b11db8e63177/go.mod h1:G1eQ/upWVdO6BGT6dlh5Yqjt+9ncH5RUAKX6UKi1F9Q=
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc=
Expand Down
Loading