Skip to content
Open
73 changes: 73 additions & 0 deletions shortcuts/base/record_json_shorthand_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT

package base

import (
"strings"
"testing"

"github.com/spf13/cobra"

"github.com/larksuite/cli/internal/cmdutil"
"github.com/larksuite/cli/shortcuts/common"
)

func mountBaseShortcutFlags(t *testing.T, s common.Shortcut, name string) *cobra.Command {
t.Helper()
parent := &cobra.Command{Use: "test"}
s.Mount(parent, &cmdutil.Factory{})
cmd, _, err := parent.Find([]string{name})
if err != nil {
t.Fatalf("Find(%s) error = %v", name, err)
}
return cmd
}

// record-list 获得 --json 简写
func TestRecordListRegistersJSONShorthand(t *testing.T) {
cmd := mountBaseShortcutFlags(t, BaseRecordList, "+record-list")
fl := cmd.Flags().Lookup("json")
if fl == nil {
t.Fatal("+record-list missing --json shorthand")
}
if fl.Usage != "shorthand for --format json" {
t.Errorf("usage = %q, want shorthand", fl.Usage)
}
if def := cmd.Flags().Lookup("format").DefValue; def != "markdown" {
t.Errorf("format default = %q, want markdown (unchanged)", def)
}
}

// record-search / record-get 的 --json 保持请求体语义,不被覆盖(回归锚点)
func TestRecordSearchGetKeepRequestBodyJSON(t *testing.T) {
for _, tc := range []struct {
name string
shortcut common.Shortcut
cmdName string
}{
{"record-search", BaseRecordSearch, "+record-search"},
{"record-get", BaseRecordGet, "+record-get"},
} {
cmd := mountBaseShortcutFlags(t, tc.shortcut, tc.cmdName)
fl := cmd.Flags().Lookup("json")
if fl == nil {
t.Fatalf("%s: --json (request body) missing", tc.name)
}
if strings.Contains(fl.Usage, "shorthand") {
t.Fatalf("%s: request-body --json overwritten by shorthand: %q", tc.name, fl.Usage)
}
if fl.Value.Type() != "string" {
t.Fatalf("%s: --json type = %q, want string", tc.name, fl.Value.Type())
}
}
}

// Enum 已接入:help 描述携带枚举后缀(框架对带 Enum 的 flag 自动追加 " (markdown|json)")
func TestRecordReadFormatFlagCarriesEnum(t *testing.T) {
cmd := mountBaseShortcutFlags(t, BaseRecordList, "+record-list")
usage := cmd.Flags().Lookup("format").Usage
if !strings.Contains(usage, "(markdown|json)") {
t.Fatalf("format usage missing enum suffix: %q", usage)
}
}
1 change: 1 addition & 0 deletions shortcuts/base/record_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ func recordReadFormatFlag() common.Flag {
return common.Flag{
Name: "format",
Default: "markdown",
Enum: []string{"markdown", "json"},
Desc: "output format: markdown (default) | json",
}
}
74 changes: 71 additions & 3 deletions shortcuts/common/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -1026,6 +1026,7 @@
}
rctx.larkSDK = sdk

applyJSONShorthand(cmd, s)
rctx.Format = rctx.Str("format")
rctx.JqExpr, _ = cmd.Flags().GetString("jq")
return rctx, nil
Expand Down Expand Up @@ -1171,6 +1172,75 @@
registerShortcutFlagsWithContext(context.Background(), cmd, f, s)
}

// shortcutDeclaresJSONFlag reports whether the shortcut itself declares a flag
// named "json" in its Flags list (custom semantics, e.g. event +subscribe's
// pretty-print switch or base +record-search's request-body payload).
// Framework-injected flags never appear in s.Flags, so this cleanly separates
// "self-declared json" from "injected shorthand".
func shortcutDeclaresJSONFlag(s *Shortcut) bool {
for _, fl := range s.Flags {
if fl.Name == "json" {
return true
}
}
return false
}

// shortcutFormatSupportsJSON reports whether the command's format flag accepts
// "json": a self-declared format supports it only when its Enum lists "json";
// a framework-injected default format (no format entry in s.Flags) always does.
func shortcutFormatSupportsJSON(s *Shortcut) bool {
for _, fl := range s.Flags {
if fl.Name == "format" {
return slices.Contains(fl.Enum, "json")
}
}
return true // framework-injected: json (default) | pretty | table | ndjson | csv
}

// ensureJSONShorthand registers --json as a shorthand for --format json when:
// 1. the command has a format flag (self-declared or framework-injected), AND
// 2. that format supports "json" (see shortcutFormatSupportsJSON), AND
// 3. no flag named "json" is registered yet — pflag panics on duplicate
// registration, and commands that declare their own --json (event
// +subscribe, base +record-search/-get) keep their custom semantics.
func ensureJSONShorthand(cmd *cobra.Command, s *Shortcut) {
// A shortcut that declares its own "json" flag defines custom semantics
// (e.g. pretty-print switch, request-body payload) — never a shorthand.
if shortcutDeclaresJSONFlag(s) {
return
}
if cmd.Flags().Lookup("format") == nil {
return

Check warning on line 1214 in shortcuts/common/runner.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/common/runner.go#L1214

Added line #L1214 was not covered by tests
}
if !shortcutFormatSupportsJSON(s) {
return
}
// Safety net: pflag panics on duplicate registration.
if cmd.Flags().Lookup("json") != nil {
return

Check warning on line 1221 in shortcuts/common/runner.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/common/runner.go#L1221

Added line #L1221 was not covered by tests
}
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
Expand Down Expand Up @@ -1234,10 +1304,8 @@
cmdutil.RegisterFlagCompletion(cmd, "format", func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
return []string{"json", "pretty", "table", "ndjson", "csv"}, cobra.ShellCompDirectiveNoFileComp
})
if cmd.Flags().Lookup("json") == nil {
cmd.Flags().Bool("json", false, "shorthand for --format json")
}
}
ensureJSONShorthand(cmd, s)
if s.Risk == "high-risk-write" {
cmd.Flags().Bool("yes", false, "confirm high-risk operation")
}
Expand Down
200 changes: 200 additions & 0 deletions shortcuts/common/runner_json_shorthand_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT

package common

import (
"context"
"testing"

"github.com/spf13/cobra"

"github.com/larksuite/cli/internal/cmdutil"
)

const jsonShorthandUsage = "shorthand for --format json"

func mountTestShortcut(t *testing.T, s Shortcut) *cobra.Command {
t.Helper()
f, _, _, _ := cmdutil.TestFactory(t, nil)
parent := &cobra.Command{Use: "root"}
s.Mount(parent, f)
cmd, _, err := parent.Find([]string{s.Command})
if err != nil {
t.Fatalf("Find() error = %v", err)
}
return cmd
}

// 自定义 format 且 Enum 含 json → 注册简写(本次修复的核心行为)
func TestJSONShorthand_CustomFormatWithJSONEnum_Registered(t *testing.T) {
cmd := mountTestShortcut(t, 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 },
})
fl := cmd.Flags().Lookup("json")
if fl == nil {
t.Fatal("--json not registered for custom-format shortcut whose Enum contains json")
}
if fl.Usage != jsonShorthandUsage {
t.Errorf("usage = %q, want %q", fl.Usage, jsonShorthandUsage)
}
// 默认输出格式不被改变
if def := cmd.Flags().Lookup("format").DefValue; def != "table" {
t.Errorf("format default = %q, want table", def)
}
}

// 自定义 format 但 Enum 不含 json → 不注册
func TestJSONShorthand_CustomFormatWithoutJSONEnum_NotRegistered(t *testing.T) {
cmd := mountTestShortcut(t, Shortcut{
Service: "x", Command: "+no-json", Description: "x",
Flags: []Flag{{Name: "format", Default: "csv", Enum: []string{"csv", "table"}, Desc: "fmt"}},
Execute: func(context.Context, *RuntimeContext) error { return nil },
})
if cmd.Flags().Lookup("json") != nil {
t.Fatal("--json must NOT be registered when format Enum lacks json")
}
}

// 自定义 format 但无 Enum(现状 triage 形态)→ 不注册(Enum 是判定依据)
func TestJSONShorthand_CustomFormatNoEnum_NotRegistered(t *testing.T) {
cmd := mountTestShortcut(t, Shortcut{
Service: "x", Command: "+legacy", Description: "x",
Flags: []Flag{{Name: "format", Default: "table", Desc: "fmt"}},
Execute: func(context.Context, *RuntimeContext) error { return nil },
})
if cmd.Flags().Lookup("json") != nil {
t.Fatal("--json must NOT be registered when format has no Enum metadata")
}
}

// 自声明 json flag(subscribe 的 pretty / record-search 的请求体)→ 不覆盖、不 panic、语义保留
func TestJSONShorthand_SelfDeclaredJSON_Preserved(t *testing.T) {
cmd := mountTestShortcut(t, 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 },
})
fl := cmd.Flags().Lookup("json")
if fl == nil {
t.Fatal("self-declared --json missing")
}
if fl.Usage != "pretty-print JSON instead of NDJSON" {
t.Errorf("self-declared --json usage overwritten: %q", fl.Usage)
}
}

// 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 优先于 --json 简写:--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{
Service: "im", Command: "+plain", Description: "x",
Execute: func(context.Context, *RuntimeContext) error { return nil },
})
fl := cmd.Flags().Lookup("json")
if fl == nil {
t.Fatal("--json missing on default-format shortcut (regression)")
}
if fl.Usage != jsonShorthandUsage {
t.Errorf("usage = %q, want %q", fl.Usage, jsonShorthandUsage)
}
}
Loading
Loading