From eb2658ca68686ea654df4ca64c89877a0b26cff3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=89=E6=BE=9C?= Date: Tue, 28 Jul 2026 21:13:29 +0800 Subject: [PATCH] fix(helpers): honor alias/env/default fallback for integer leaf flags The leaf fallback chain read only string flags, so LeafInt/LeafInt64 flags could never satisfy Required via alias or env, alias values for integer flags were silently dropped, and a registered Default shadowed alias/env values. Resolution order is now explicit flag > alias > env > Default > ArgDefault, aliases register with the primary flag's Kind, and unparsable integer env values fail loudly. --- internal/helpers/leaf.go | 87 +++++++++++++---- internal/helpers/leaf_dispatch_test.go | 124 +++++++++++++++++++++++++ 2 files changed, 192 insertions(+), 19 deletions(-) diff --git a/internal/helpers/leaf.go b/internal/helpers/leaf.go index 710cd397..4ebe5933 100644 --- a/internal/helpers/leaf.go +++ b/internal/helpers/leaf.go @@ -16,6 +16,7 @@ package helpers import ( "fmt" "os" + "strconv" "strings" "github.com/spf13/cobra" @@ -53,7 +54,7 @@ type LeafFlag struct { Name string // flag 名(kebab-case) Usage string // 注册 usage 文案 Kind LeafFlagKind // 值类型,默认 LeafString - Default string // 注册默认值(仅 LeafString 有效) + Default string // 注册默认值(仅 LeafString 有效);回退链链尾兜底,不遮蔽别名/env // Required 为 true 时在 RunE 期校验有效值非空。普通 Required 汇聚为 // cmdutil.ValidateRequiredFlags 兼容的统一报错;配置 EnvVar 时回退读 @@ -64,8 +65,8 @@ type LeafFlag struct { // 投影的硬下限),cobra 会在 RunE 之前先行报错。 MarkRequired bool - Aliases []string // 隐藏别名,主 flag 为空时按序回退(字符串) - EnvVar string // 有效值为空时回退读取的环境变量 + Aliases []string // 隐藏别名,按主 flag 的 Kind 注册;主 flag 未显式提供时按序回退 + EnvVar string // 有效值为空时回退读取的环境变量(整型 flag 的 env 值必须可解析) // ArgDefault:注册默认值为空、但 toolArgs 需要兜底的场景(如 list type // 注册默认 "ALL" 之外的旧命令);有效值为空时以 ArgDefault 入参。 ArgDefault string @@ -134,8 +135,16 @@ func NewLeafCommand(spec LeafSpec) *cobra.Command { default: cmd.Flags().String(flag.Name, flag.Default, flag.Usage) } + // 别名按主 flag 的 Kind 注册,否则整型别名的值永远读不到(静默丢弃)。 for _, alias := range flag.Aliases { - cmd.Flags().String(alias, "", flag.Usage+" (alias)") + switch flag.Kind { + case LeafInt64: + cmd.Flags().Int64(alias, 0, flag.Usage+" (alias)") + case LeafInt: + cmd.Flags().Int(alias, 0, flag.Usage+" (alias)") + default: + cmd.Flags().String(alias, "", flag.Usage+" (alias)") + } _ = cmd.Flags().MarkHidden(alias) } if flag.MarkRequired { @@ -210,15 +219,17 @@ func leafArgs(cmd *cobra.Command, spec LeafSpec) (map[string]any, error) { if bind == "" { bind = flag.Name } - if flag.Kind == LeafInt64 { - if v, _ := cmd.Flags().GetInt64(flag.Name); v > 0 { - toolArgs[bind] = v + if flag.Kind == LeafInt64 || flag.Kind == LeafInt { + v, err := leafIntegerValue(cmd, flag) + if err != nil { + return nil, err } - continue - } - if flag.Kind == LeafInt { - if v, _ := cmd.Flags().GetInt(flag.Name); v != 0 { + // LeafInt64 保持「值 > 0 才入参」(分页游标语义);LeafInt 保持 + // 「非零才入参」(putInt 语义)。 + if flag.Kind == LeafInt64 && v > 0 { toolArgs[bind] = v + } else if flag.Kind == LeafInt && v != 0 { + toolArgs[bind] = int(v) } continue } @@ -245,8 +256,9 @@ func leafArgs(cmd *cobra.Command, spec LeafSpec) (map[string]any, error) { return toolArgs, nil } -// leafEffectiveValue 按「主 flag → 别名 → 环境变量」顺序取有效值;Trim 为 -// true 时对结果统一 TrimSpace。 +// leafEffectiveValue 按「显式主 flag → 别名 → 环境变量 → 注册默认值」顺序取 +// 有效值(字符串形态,整型 flag 统一格式化);Trim 为 true 时对结果统一 +// TrimSpace。 func leafEffectiveValue(cmd *cobra.Command, flag LeafFlag) string { v := leafRawValue(cmd, flag) if flag.Trim { @@ -255,18 +267,55 @@ func leafEffectiveValue(cmd *cobra.Command, flag LeafFlag) string { return v } -// leafRawValue 取未 trim 的原始有效值(主 flag → 别名 → 环境变量)。 +// leafRawValue 取未 trim 的原始有效值。主 flag 仅在用户显式提供(Changed) +// 且非空时命中;注册默认值降级为链尾兜底,不再遮蔽别名与环境变量。 func leafRawValue(cmd *cobra.Command, flag LeafFlag) string { - if v := mustGetFlag(cmd, flag.Name); v != "" { - return v + if cmd.Flags().Changed(flag.Name) { + if v := leafFlagString(cmd, flag.Kind, flag.Name); v != "" { + return v + } } for _, alias := range flag.Aliases { - if v, _ := cmd.Flags().GetString(alias); v != "" { + if !cmd.Flags().Changed(alias) { + continue + } + if v := leafFlagString(cmd, flag.Kind, alias); v != "" { return v } } if flag.EnvVar != "" { - return os.Getenv(flag.EnvVar) + if v := os.Getenv(flag.EnvVar); v != "" { + return v + } } - return "" + return flag.Default +} + +// leafFlagString 按注册类型读取 flag 并统一为字符串形态,使整型 flag 能复用 +// 同一条回退链(required 校验、别名、env)。 +func leafFlagString(cmd *cobra.Command, kind LeafFlagKind, name string) string { + switch kind { + case LeafInt64: + v, _ := cmd.Flags().GetInt64(name) + return strconv.FormatInt(v, 10) + case LeafInt: + v, _ := cmd.Flags().GetInt(name) + return strconv.Itoa(v) + default: + return mustGetFlag(cmd, name) + } +} + +// leafIntegerValue 按回退链取整型 flag 的有效值;环境变量提供的字符串必须 +// 可解析,否则报错而非静默丢弃。 +func leafIntegerValue(cmd *cobra.Command, flag LeafFlag) (int64, error) { + raw := leafEffectiveValue(cmd, flag) + if raw == "" { + return 0, nil + } + v, err := strconv.ParseInt(raw, 10, 64) + if err != nil { + return 0, fmt.Errorf("flag --%s: invalid integer value %q", flag.Name, raw) + } + return v, nil } diff --git a/internal/helpers/leaf_dispatch_test.go b/internal/helpers/leaf_dispatch_test.go index 21261a33..09adce87 100644 --- a/internal/helpers/leaf_dispatch_test.go +++ b/internal/helpers/leaf_dispatch_test.go @@ -116,3 +116,127 @@ func TestLeafValidateRequiredEnvDefaultHint(t *testing.T) { t.Fatalf("leafValidateRequired() = %v, want default hint", err) } } + +// ── CR 探针回归:A1/A2/A3 ────────────────────────────────────────────── + +// TestLeafIntRequiredAcceptsExplicitValue(A1 探针):LeafInt + Required +// 传了值必须通过校验,不得报「missing required flag(s)」。 +func TestLeafIntRequiredAcceptsExplicitValue(t *testing.T) { + spec := LeafSpec{ + Use: "list", Tool: "list_thing", + Flags: []LeafFlag{{Name: "n", Usage: "数量", Kind: LeafInt, Required: true, Bind: "count"}}, + } + cmd := NewLeafCommand(spec) + if err := cmd.Flags().Set("n", "5"); err != nil { + t.Fatal(err) + } + if err := leafValidateRequired(cmd, spec); err != nil { + t.Fatalf("leafValidateRequired() = %v, want nil for --n 5", err) + } + args, err := leafArgs(cmd, spec) + if err != nil { + t.Fatal(err) + } + if got := args["count"]; got != 5 { + t.Fatalf("count = %v (%T), want int 5", got, got) + } + // 未提供值时仍按主 flag 名报缺失。 + missing := NewLeafCommand(spec) + if err := leafValidateRequired(missing, spec); err == nil || !strings.Contains(err.Error(), "missing required flag(s): --n") { + t.Fatalf("leafValidateRequired() = %v, want missing --n", err) + } +} + +// TestLeafIntAliasAndEnvFallback(A2 探针):整型 flag 的别名与 env 回退 +// 必须生效,不得静默丢值。 +func TestLeafIntAliasAndEnvFallback(t *testing.T) { + spec := LeafSpec{ + Use: "list", Tool: "list_thing", + Flags: []LeafFlag{{Name: "page-size", Usage: "分页大小", Kind: LeafInt, Aliases: []string{"limit"}, EnvVar: "DWS_LEAF_TEST_PAGE_SIZE", Bind: "pageSize"}}, + } + // 别名提供值。 + cmd := NewLeafCommand(spec) + if err := cmd.Flags().Set("limit", "7"); err != nil { + t.Fatal(err) + } + args, err := leafArgs(cmd, spec) + if err != nil { + t.Fatal(err) + } + if got := args["pageSize"]; got != 7 { + t.Fatalf("pageSize = %v (%T), want int 7 from alias", got, got) + } + // env 提供值。 + t.Setenv("DWS_LEAF_TEST_PAGE_SIZE", "9") + cmd = NewLeafCommand(spec) + args, err = leafArgs(cmd, spec) + if err != nil { + t.Fatal(err) + } + if got := args["pageSize"]; got != 9 { + t.Fatalf("pageSize = %v (%T), want int 9 from env", got, got) + } + // env 值不可解析必须报错而非静默丢弃。 + t.Setenv("DWS_LEAF_TEST_PAGE_SIZE", "not-a-number") + cmd = NewLeafCommand(spec) + if _, err := leafArgs(cmd, spec); err == nil || !strings.Contains(err.Error(), "invalid integer value") { + t.Fatalf("leafArgs() = %v, want parse error for garbage env", err) + } +} + +// TestLeafInt64AliasRegisteredTyped:LeafInt64 别名按 Kind 注册且回退生效。 +func TestLeafInt64AliasRegisteredTyped(t *testing.T) { + spec := LeafSpec{ + Use: "list", Tool: "list_thing", + Flags: []LeafFlag{{Name: "cursor", Usage: "游标", Kind: LeafInt64, Aliases: []string{"offset"}, Bind: "cursor"}}, + } + cmd := NewLeafCommand(spec) + if f := cmd.Flags().Lookup("offset"); f == nil || f.Value.Type() != "int64" { + t.Fatalf("alias offset = %+v, want registered as int64", f) + } + if err := cmd.Flags().Set("offset", "11"); err != nil { + t.Fatal(err) + } + args, err := leafArgs(cmd, spec) + if err != nil { + t.Fatal(err) + } + if got := args["cursor"]; got != int64(11) { + t.Fatalf("cursor = %v (%T), want int64 11 from alias", got, got) + } +} + +// TestLeafDefaultDoesNotShadowFallback(A3 探针):注册默认值不得遮蔽 +// 别名与环境变量,只能作为链尾兜底。 +func TestLeafDefaultDoesNotShadowFallback(t *testing.T) { + spec := LeafSpec{ + Use: "list", Tool: "list_thing", + Flags: []LeafFlag{{Name: "type", Usage: "类型", Default: "ALL", Aliases: []string{"kind"}, EnvVar: "DWS_LEAF_TEST_TYPE", Bind: "type"}}, + } + // env 覆盖注册默认值。 + t.Setenv("DWS_LEAF_TEST_TYPE", "from-env") + cmd := NewLeafCommand(spec) + if got := leafEffectiveValue(cmd, spec.Flags[0]); got != "from-env" { + t.Fatalf("effective = %q, want env to beat registered default", got) + } + // 别名覆盖 env 与默认值。 + if err := cmd.Flags().Set("kind", "from-alias"); err != nil { + t.Fatal(err) + } + if got := leafEffectiveValue(cmd, spec.Flags[0]); got != "from-alias" { + t.Fatalf("effective = %q, want alias to beat env", got) + } + // 显式主 flag 最高优先。 + if err := cmd.Flags().Set("type", "explicit"); err != nil { + t.Fatal(err) + } + if got := leafEffectiveValue(cmd, spec.Flags[0]); got != "explicit" { + t.Fatalf("effective = %q, want explicit primary to win", got) + } + // 全部缺席时回落注册默认值。 + bare := NewLeafCommand(spec) + t.Setenv("DWS_LEAF_TEST_TYPE", "") + if got := leafEffectiveValue(bare, spec.Flags[0]); got != "ALL" { + t.Fatalf("effective = %q, want registered default as tail fallback", got) + } +}