From fc103382535a471af4e915fcfbd8e4ab02ca855c Mon Sep 17 00:00:00 2001 From: zhengzhijie Date: Wed, 3 Jun 2026 16:34:20 +0800 Subject: [PATCH] fix(cli): align flag-before-subcommand unknown_flag detail schema MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The flag-before-subcommand recovery path emitted a Type: unknown_flag whose detail only carried unknown_flags + command_path, diverging from flagDidYouMean's unknown_flag detail (unknown, command_path, suggestions, valid_flags). A consumer keyed on Type then saw two shapes for one Type. Emit the same keys from both paths: add unknown (the offending flag; joined when multiple), plus empty suggestions/valid_flags — the subcommand isn't resolved at this point, so there is no meaningful flag universe to suggest from, and the group's own flags would mislead. unknown_flags is retained as the authoritative multi-flag field. Test locks the shared schema. --- cmd/root.go | 10 ++++++++++ cmd/unknown_subcommand_test.go | 25 +++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/cmd/root.go b/cmd/root.go index eebbfa67c..425bded7a 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -391,8 +391,18 @@ func unknownSubcommandRunE(cmd *cobra.Command, args []string) error { Message: fmt.Sprintf("unknown flag %s before a subcommand for %q", strings.Join(unknown, ", "), cmd.CommandPath()), Hint: fmt.Sprintf("flags belong to a subcommand; run `%s --help` to list subcommands and their flags", cmd.CommandPath()), Detail: map[string]any{ + // Keep the same detail keys as flagDidYouMean's unknown_flag + // so a consumer keyed on Type can read a stable shape. The + // subcommand isn't resolved here, so suggestions/valid_flags + // have no meaningful universe to draw from — emit empty + // rather than the group's own (misleading) flags. unknown is + // the back-compat singular field; unknown_flags carries the + // full list when more than one flag was supplied. + "unknown": strings.Join(unknown, ", "), "unknown_flags": unknown, "command_path": cmd.CommandPath(), + "suggestions": []string{}, + "valid_flags": []string{}, }, }, } diff --git a/cmd/unknown_subcommand_test.go b/cmd/unknown_subcommand_test.go index 7f68fdaaa..e765341ea 100644 --- a/cmd/unknown_subcommand_test.go +++ b/cmd/unknown_subcommand_test.go @@ -125,6 +125,31 @@ func TestUnknownSubcommandRunE_FlagBeforeSubcommandIsStructured(t *testing.T) { if !strings.Contains(err.Error(), "unknown flag") { t.Errorf("error = %q, want it to mention an unknown flag", err.Error()) } + + // The detail must stay schema-compatible with flagDidYouMean's unknown_flag + // (same Type → same keys), so a consumer keyed on Type reads a stable shape. + exitErr, ok := err.(*output.ExitError) + if !ok || exitErr.Detail == nil { + t.Fatalf("expected *output.ExitError with Detail, got %T", err) + } + if exitErr.Detail.Type != "unknown_flag" { + t.Errorf("detail.Type = %q, want unknown_flag", exitErr.Detail.Type) + } + detail, ok := exitErr.Detail.Detail.(map[string]any) + if !ok { + t.Fatalf("expected detail to be map[string]any, got %T", exitErr.Detail.Detail) + } + if detail["unknown"] != "--badflag" { + t.Errorf("detail.unknown = %v, want --badflag", detail["unknown"]) + } + if got, _ := detail["unknown_flags"].([]string); len(got) != 1 || got[0] != "--badflag" { + t.Errorf("detail.unknown_flags = %v, want [--badflag]", detail["unknown_flags"]) + } + for _, key := range []string{"suggestions", "valid_flags"} { + if _, present := detail[key]; !present { + t.Errorf("detail.%s missing; must be present (empty) to match the unknown_flag schema", key) + } + } } func TestUnknownSubcommandRunE_NoArgsShowsHelp(t *testing.T) {