From 490b5923404fef63f08f6ea7650abe3a8d4ad01d Mon Sep 17 00:00:00 2001 From: Evan Phoenix Date: Fri, 11 Sep 2026 11:15:36 -0700 Subject: [PATCH 1/5] Pin mflags to the nicer unknown-command commit Brings in the mistyped-command suggestions for MIR-1823. A wrong command now names the word that went wrong and offers the closest real one, instead of reporting "error parsing flags: unexpected arguments: [x]". --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index b489951a0..f8ff40808 100644 --- a/go.mod +++ b/go.mod @@ -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-20260911181117-fad72f0d8fc7 modernc.org/sqlite v1.45.0 sigs.k8s.io/knftables v0.0.21 ) diff --git a/go.sum b/go.sum index 9ca6bbd5a..3075e6b90 100644 --- a/go.sum +++ b/go.sum @@ -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-20260911181117-fad72f0d8fc7 h1:DchPEeYVEzaJ3MuhgXh1i4Ok/Y2owxazqf2K5xQVXSk= +miren.dev/mflags v0.0.0-20260911181117-fad72f0d8fc7/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= From 3428915e5177c7a9d73d80b39df272148b27e64c Mon Sep 17 00:00:00 2001 From: Evan Phoenix Date: Fri, 11 Sep 2026 11:15:39 -0700 Subject: [PATCH 2/5] Cover the mistyped-command messages from the CLI side MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The wording is the product here, so pin it against the real command tree rather than only against mflags' synthetic test dispatcher. Covers the three shapes that used to fail differently: a word matching nothing, a word under a command with sub-commands, and a word under a section, which previously printed help and exited 0. The companion test guards what the check must not touch — help keywords, pass-through arguments, and global flags that take a value. --- cli/commands/unknown_command_test.go | 113 +++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 cli/commands/unknown_command_test.go diff --git a/cli/commands/unknown_command_test.go b/cli/commands/unknown_command_test.go new file mode 100644 index 000000000..ee8c0afb6 --- /dev/null +++ b/cli/commands/unknown_command_test.go @@ -0,0 +1,113 @@ +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) + } + } + }) + } +} + +// 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) + } + } + }) + } +} From f7731e900c4b8999600d9464eaf62e6b00408600 Mon Sep 17 00:00:00 2001 From: Evan Phoenix Date: Fri, 11 Sep 2026 16:01:19 -0700 Subject: [PATCH 3/5] Suggest a flag name when one is mistyped Re-pins mflags to pick up the same treatment for flags: unknown flag: --naem Did you mean? --name Long flags only; a single letter is too little to guess from. The mflags bump also makes a transposition cost one edit instead of two, which is what lets "naem" reach "name" at all, and improves the command suggestions for the same reason. --- cli/commands/unknown_command_test.go | 49 ++++++++++++++++++++++++++++ go.mod | 2 +- go.sum | 4 +-- 3 files changed, 52 insertions(+), 3 deletions(-) diff --git a/cli/commands/unknown_command_test.go b/cli/commands/unknown_command_test.go index ee8c0afb6..dc18c8e7d 100644 --- a/cli/commands/unknown_command_test.go +++ b/cli/commands/unknown_command_test.go @@ -86,6 +86,55 @@ func TestUnknownCommandSuggests(t *testing.T) { } } +// 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?"}, + }, + } + + 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. diff --git a/go.mod b/go.mod index f8ff40808..b90ccb851 100644 --- a/go.mod +++ b/go.mod @@ -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-20260911181117-fad72f0d8fc7 + miren.dev/mflags v0.0.0-20260911230014-0b1a73e4205a modernc.org/sqlite v1.45.0 sigs.k8s.io/knftables v0.0.21 ) diff --git a/go.sum b/go.sum index 3075e6b90..36c2ffd60 100644 --- a/go.sum +++ b/go.sum @@ -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-20260911181117-fad72f0d8fc7 h1:DchPEeYVEzaJ3MuhgXh1i4Ok/Y2owxazqf2K5xQVXSk= -miren.dev/mflags v0.0.0-20260911181117-fad72f0d8fc7/go.mod h1:G1eQ/upWVdO6BGT6dlh5Yqjt+9ncH5RUAKX6UKi1F9Q= +miren.dev/mflags v0.0.0-20260911230014-0b1a73e4205a h1:e5BZgwWX++3oThOcH1Y6GSMjCdrYT+XCKBbLj7iOzlI= +miren.dev/mflags v0.0.0-20260911230014-0b1a73e4205a/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= From 3aadc5383e6e61ca73cbe958595ff503ed823104 Mon Sep 17 00:00:00 2001 From: Evan Phoenix Date: Fri, 11 Sep 2026 16:26:12 -0700 Subject: [PATCH 4/5] Re-pin mflags for the review fixes Picks up three fixes from review on mirendev/mflags#17: flag-only input such as `miren --bogus` now names the flag instead of reporting an empty command name, hidden flags stay out of suggestions, and prefix completions rank by characters rather than bytes. Adds the flag-only case here too, since it is user-visible and was a regression against the previous release. --- cli/commands/unknown_command_test.go | 7 +++++++ go.mod | 2 +- go.sum | 4 ++-- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/cli/commands/unknown_command_test.go b/cli/commands/unknown_command_test.go index dc18c8e7d..3082c9673 100644 --- a/cli/commands/unknown_command_test.go +++ b/cli/commands/unknown_command_test.go @@ -111,6 +111,13 @@ func TestUnknownFlagSuggests(t *testing.T) { 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 { diff --git a/go.mod b/go.mod index b90ccb851..ea2fc34db 100644 --- a/go.mod +++ b/go.mod @@ -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-20260911230014-0b1a73e4205a + miren.dev/mflags v0.0.0-20260911232458-163db4e8274b modernc.org/sqlite v1.45.0 sigs.k8s.io/knftables v0.0.21 ) diff --git a/go.sum b/go.sum index 36c2ffd60..6a777d8fe 100644 --- a/go.sum +++ b/go.sum @@ -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-20260911230014-0b1a73e4205a h1:e5BZgwWX++3oThOcH1Y6GSMjCdrYT+XCKBbLj7iOzlI= -miren.dev/mflags v0.0.0-20260911230014-0b1a73e4205a/go.mod h1:G1eQ/upWVdO6BGT6dlh5Yqjt+9ncH5RUAKX6UKi1F9Q= +miren.dev/mflags v0.0.0-20260911232458-163db4e8274b h1:ei07ZHgKqoi5JwzOeeZ2UOdQMn8bSQWfhVfLoPt/gTU= +miren.dev/mflags v0.0.0-20260911232458-163db4e8274b/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= From 92bb50e66ca78ede63fba877023567ffea385509 Mon Sep 17 00:00:00 2001 From: Evan Phoenix Date: Sat, 12 Sep 2026 19:40:38 -0700 Subject: [PATCH 5/5] Pin mflags to the merged commit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mirendev/mflags#17 is merged; repoint from the branch commit to main. No behavior change — same code, now reachable from mflags main. --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index ea2fc34db..d5d554bc1 100644 --- a/go.mod +++ b/go.mod @@ -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-20260911232458-163db4e8274b + miren.dev/mflags v0.0.0-20260913023833-b11db8e63177 modernc.org/sqlite v1.45.0 sigs.k8s.io/knftables v0.0.21 ) diff --git a/go.sum b/go.sum index 6a777d8fe..c85ed972c 100644 --- a/go.sum +++ b/go.sum @@ -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-20260911232458-163db4e8274b h1:ei07ZHgKqoi5JwzOeeZ2UOdQMn8bSQWfhVfLoPt/gTU= -miren.dev/mflags v0.0.0-20260911232458-163db4e8274b/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=