From 5efa173f0bf76b94f2e5f0ca5fa5f32b1043fc86 Mon Sep 17 00:00:00 2001 From: "zhaojunlin.0405" Date: Thu, 2 Jul 2026 17:48:57 +0800 Subject: [PATCH] fix: fold --json shorthand into format flag before consumption --- shortcuts/common/runner.go | 19 ++++ .../common/runner_json_shorthand_test.go | 96 +++++++++++++++++++ 2 files changed, 115 insertions(+) diff --git a/shortcuts/common/runner.go b/shortcuts/common/runner.go index b3be6c308..981420175 100644 --- a/shortcuts/common/runner.go +++ b/shortcuts/common/runner.go @@ -1026,6 +1026,7 @@ func newRuntimeContext(cmd *cobra.Command, f *cmdutil.Factory, s *Shortcut, conf } rctx.larkSDK = sdk + applyJSONShorthand(cmd, s) rctx.Format = rctx.Str("format") rctx.JqExpr, _ = cmd.Flags().GetString("jq") return rctx, nil @@ -1222,6 +1223,24 @@ func ensureJSONShorthand(cmd *cobra.Command, s *Shortcut) { cmd.Flags().Bool("json", false, "shorthand for --format json") } +// applyJSONShorthand folds the injected --json shorthand into the format flag +// itself, before rctx.Format caches it — so both the cached value (OutFormat, +// ValidateJqFlags, dry-run) and later runtime.Str("format") reads observe +// "json". An explicitly passed --format always wins over the shorthand (the +// shorthand only fills in when the user did not choose a format). Shortcuts +// that declare their own "json" flag keep its custom semantics untouched. +func applyJSONShorthand(cmd *cobra.Command, s *Shortcut) { + if shortcutDeclaresJSONFlag(s) { + return + } + if cmd.Flags().Lookup("json") == nil || cmd.Flags().Changed("format") { + return + } + if set, _ := cmd.Flags().GetBool("json"); set { + _ = cmd.Flags().Set("format", "json") + } +} + func registerShortcutFlagsWithContext(ctx context.Context, cmd *cobra.Command, f *cmdutil.Factory, s *Shortcut) { for _, fl := range s.Flags { desc := fl.Desc diff --git a/shortcuts/common/runner_json_shorthand_test.go b/shortcuts/common/runner_json_shorthand_test.go index 34239cf52..e4cdf420c 100644 --- a/shortcuts/common/runner_json_shorthand_test.go +++ b/shortcuts/common/runner_json_shorthand_test.go @@ -88,6 +88,102 @@ func TestJSONShorthand_SelfDeclaredJSON_Preserved(t *testing.T) { } } +// parseMounted mounts the shortcut and parses args against the command's FlagSet +// (registration side effects included), without executing RunE. +func parseMounted(t *testing.T, s Shortcut, args []string) *cobra.Command { + t.Helper() + cmd := mountTestShortcut(t, s) + if err := cmd.ParseFlags(args); err != nil { + t.Fatalf("ParseFlags(%v) error = %v", args, err) + } + return cmd +} + +func customFormatShortcut() Shortcut { + return Shortcut{ + Service: "mail", Command: "+fake-triage", Description: "x", + Flags: []Flag{{Name: "format", Default: "table", Enum: []string{"table", "json", "data"}, Desc: "fmt"}}, + Execute: func(context.Context, *RuntimeContext) error { return nil }, + } +} + +// --json 单独使用 → format 归一化为 json +func TestApplyJSONShorthand_JSONAlone_SetsFormatJSON(t *testing.T) { + s := customFormatShortcut() + cmd := parseMounted(t, s, []string{"--json"}) + applyJSONShorthand(cmd, &s) + if got := cmd.Flags().Lookup("format").Value.String(); got != "json" { + t.Fatalf("format = %q, want json", got) + } +} + +// 显式 --format 优先(D2 决策):--format table --json → table +func TestApplyJSONShorthand_ExplicitFormatWins(t *testing.T) { + s := customFormatShortcut() + cmd := parseMounted(t, s, []string{"--format", "table", "--json"}) + applyJSONShorthand(cmd, &s) + if got := cmd.Flags().Lookup("format").Value.String(); got != "table" { + t.Fatalf("format = %q, want table (explicit --format must win)", got) + } +} + +// --format json --json → json(一致,无冲突) +func TestApplyJSONShorthand_ExplicitJSONFormatConsistent(t *testing.T) { + s := customFormatShortcut() + cmd := parseMounted(t, s, []string{"--format", "json", "--json"}) + applyJSONShorthand(cmd, &s) + if got := cmd.Flags().Lookup("format").Value.String(); got != "json" { + t.Fatalf("format = %q, want json", got) + } +} + +// 均不传 → 默认值不变 +func TestApplyJSONShorthand_NoFlags_DefaultUntouched(t *testing.T) { + s := customFormatShortcut() + cmd := parseMounted(t, s, nil) + applyJSONShorthand(cmd, &s) + if got := cmd.Flags().Lookup("format").Value.String(); got != "table" { + t.Fatalf("format = %q, want table (default untouched)", got) + } +} + +// 自声明 string 型 --json(record-search 形态:format+json 双声明)→ 归一化跳过 +func TestApplyJSONShorthand_SelfDeclaredStringJSON_Skipped(t *testing.T) { + s := Shortcut{ + Service: "base", Command: "+fake-record-search", Description: "x", + Flags: []Flag{ + {Name: "format", Default: "markdown", Enum: []string{"markdown", "json"}, Desc: "fmt"}, + {Name: "json", Desc: "request body JSON object"}, + }, + Execute: func(context.Context, *RuntimeContext) error { return nil }, + } + cmd := parseMounted(t, s, []string{"--json", `{"keyword":"Alice"}`}) + applyJSONShorthand(cmd, &s) + if got := cmd.Flags().Lookup("format").Value.String(); got != "markdown" { + t.Fatalf("format = %q, want markdown (self-declared json must not normalize)", got) + } + if got := cmd.Flags().Lookup("json").Value.String(); got != `{"keyword":"Alice"}` { + t.Fatalf("request-body --json corrupted: %q", got) + } +} + +// 自声明 bool 型 --json(subscribe 形态:无自定义 format,框架注入 format)→ 归一化跳过 +func TestApplyJSONShorthand_SelfDeclaredBoolJSON_Skipped(t *testing.T) { + s := Shortcut{ + Service: "event", Command: "+fake-subscribe", Description: "x", + Flags: []Flag{ + {Name: "json", Type: "bool", Desc: "pretty-print JSON instead of NDJSON"}, + }, + Execute: func(context.Context, *RuntimeContext) error { return nil }, + } + cmd := parseMounted(t, s, []string{"--json"}) + applyJSONShorthand(cmd, &s) + // 注入的 format 默认即 json;这里断言的是 Changed 状态未被归一化污染 + if cmd.Flags().Changed("format") { + t.Fatal("normalization must not touch format for shortcuts declaring their own --json") + } +} + // 无自定义 format(普通命令)→ 注入默认 format + 简写(现状回归) func TestJSONShorthand_DefaultInjectedFormat_StillRegistered(t *testing.T) { cmd := mountTestShortcut(t, Shortcut{