From a6fd563866eba08b0bd1ecffc87124642cffeabd Mon Sep 17 00:00:00 2001 From: luozhixiong Date: Fri, 17 Jul 2026 17:00:08 +0800 Subject: [PATCH] fix(im): pin explicit identity on user-only examples and run them verbatim --- shortcuts/im/im_feed_group_list_item.go | 2 +- shortcuts/im/im_feed_group_query_item.go | 2 +- shortcuts/im/im_feed_shortcut_create.go | 4 +- shortcuts/im/im_feed_shortcut_remove.go | 2 +- shortcuts/im/im_flag_cancel.go | 2 +- shortcuts/im/im_flag_create.go | 4 +- shortcuts/im/im_messages_search.go | 4 +- shortcuts/im/tips_examples_test.go | 26 +++++++++ tests/cli_e2e/im/tips_examples_dryrun_test.go | 58 ++++++++----------- 9 files changed, 61 insertions(+), 43 deletions(-) diff --git a/shortcuts/im/im_feed_group_list_item.go b/shortcuts/im/im_feed_group_list_item.go index 0b5039917..7d0e6349c 100644 --- a/shortcuts/im/im_feed_group_list_item.go +++ b/shortcuts/im/im_feed_group_list_item.go @@ -36,7 +36,7 @@ var ImFeedGroupListItem = common.Shortcut{ {Name: "end-time", Desc: "update-time window end (Unix milliseconds as a decimal string)"}, }, Tips: []string{ - `Example: lark-cli im +feed-group-list-item --feed-group-id `, + `Example: lark-cli im +feed-group-list-item --feed-group-id --as user`, }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { return validateFeedGroupListOptions(runtime) diff --git a/shortcuts/im/im_feed_group_query_item.go b/shortcuts/im/im_feed_group_query_item.go index 200b49de4..0939ac87a 100644 --- a/shortcuts/im/im_feed_group_query_item.go +++ b/shortcuts/im/im_feed_group_query_item.go @@ -28,7 +28,7 @@ var ImFeedGroupQueryItem = common.Shortcut{ {Name: "feed-id", Desc: "comma-separated chat IDs (oc_xxx); feed_type is fixed to chat (required)"}, }, Tips: []string{ - `Example: lark-cli im +feed-group-query-item --feed-group-id --feed-id `, + `Example: lark-cli im +feed-group-query-item --feed-group-id --feed-id --as user`, }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { _, err := buildFeedGroupQueryItemBody(runtime) diff --git a/shortcuts/im/im_feed_shortcut_create.go b/shortcuts/im/im_feed_shortcut_create.go index 7ea2ab439..7eee0b65b 100644 --- a/shortcuts/im/im_feed_shortcut_create.go +++ b/shortcuts/im/im_feed_shortcut_create.go @@ -35,8 +35,8 @@ var ImFeedShortcutCreate = common.Shortcut{ Desc: "append at the bottom of the shortcut list; mutually exclusive with --head"}, }, Tips: []string{ - `Example: lark-cli im +feed-shortcut-create --chat-id `, - `Example: lark-cli im +feed-shortcut-create --chat-id --tail`, + `Example: lark-cli im +feed-shortcut-create --chat-id --as user`, + `Example: lark-cli im +feed-shortcut-create --chat-id --tail --as user`, }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { if _, err := collectChatIDs(runtime); err != nil { diff --git a/shortcuts/im/im_feed_shortcut_remove.go b/shortcuts/im/im_feed_shortcut_remove.go index cf2f1cf22..4816abb32 100644 --- a/shortcuts/im/im_feed_shortcut_remove.go +++ b/shortcuts/im/im_feed_shortcut_remove.go @@ -29,7 +29,7 @@ var ImFeedShortcutRemove = common.Shortcut{ Desc: "open_chat_id to remove from feed shortcuts (oc_xxx); required; repeat the flag or pass comma-separated; max 10 per call"}, }, Tips: []string{ - `Example: lark-cli im +feed-shortcut-remove --chat-id ,`, + `Example: lark-cli im +feed-shortcut-remove --chat-id , --as user`, }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { _, err := collectChatIDs(runtime) diff --git a/shortcuts/im/im_flag_cancel.go b/shortcuts/im/im_flag_cancel.go index aeb4c3631..aa9b697dd 100644 --- a/shortcuts/im/im_flag_cancel.go +++ b/shortcuts/im/im_flag_cancel.go @@ -28,7 +28,7 @@ var ImFlagCancel = common.Shortcut{ {Name: "flag-type", Desc: "flag type override: message|feed; omit to double-cancel both layers"}, }, Tips: []string{ - `Example: lark-cli im +flag-cancel --message-id `, + `Example: lark-cli im +flag-cancel --message-id --as user`, }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { _, _, err := buildCancelItemsForPreview(runtime) diff --git a/shortcuts/im/im_flag_create.go b/shortcuts/im/im_flag_create.go index 080b36974..c3c1f19ee 100644 --- a/shortcuts/im/im_flag_create.go +++ b/shortcuts/im/im_flag_create.go @@ -27,8 +27,8 @@ var ImFlagCreate = common.Shortcut{ {Name: "flag-type", Desc: "flag type: message (default) or feed"}, }, Tips: []string{ - `Example: lark-cli im +flag-create --message-id `, - `Example: lark-cli im +flag-create --message-id --flag-type feed`, + `Example: lark-cli im +flag-create --message-id --as user`, + `Example: lark-cli im +flag-create --message-id --flag-type feed --as user`, }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { _, err := buildCreateItemForPreview(runtime) diff --git a/shortcuts/im/im_messages_search.go b/shortcuts/im/im_messages_search.go index fcfec9425..6ca52683f 100644 --- a/shortcuts/im/im_messages_search.go +++ b/shortcuts/im/im_messages_search.go @@ -52,8 +52,8 @@ var ImMessagesSearch = common.Shortcut{ {Name: "no-reactions", Type: "bool", Desc: "skip auto-fetching reactions for each message (default: enrichment enabled)"}, }, Tips: []string{ - `Example: lark-cli im +messages-search --query "keyword"`, - `Example: lark-cli im +messages-search --query "keyword" --chat-id --start 2026-07-01 --end 2026-07-08`, + `Example: lark-cli im +messages-search --query "keyword" --as user`, + `Example: lark-cli im +messages-search --query "keyword" --chat-id --start 2026-07-01 --end 2026-07-08 --as user`, }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { req, err := buildMessagesSearchRequest(runtime) diff --git a/shortcuts/im/tips_examples_test.go b/shortcuts/im/tips_examples_test.go index 104c3ce5f..68c13671f 100644 --- a/shortcuts/im/tips_examples_test.go +++ b/shortcuts/im/tips_examples_test.go @@ -95,6 +95,32 @@ func TestIMTipsExampleFlagsExist(t *testing.T) { } } +// TestIMTipsExamplesPinIdentity locks the identity convention on copyable +// examples: user-only shortcuts must pin --as user (a bot-default +// environment would otherwise reject the copied command), and the outbound +// send/reply shortcuts must pin --as bot (governance: never rely on the +// local default identity for deliveries). +func TestIMTipsExamplesPinIdentity(t *testing.T) { + outbound := map[string]bool{"+messages-send": true, "+messages-reply": true} + for _, cmd := range tipsExampleTargets { + sc := shortcutByCommand(t, cmd) + botCapable := false + for _, a := range sc.AuthTypes { + if a == "bot" { + botCapable = true + } + } + for _, example := range exampleCommands(sc) { + if !botCapable && !strings.Contains(example, "--as user") { + t.Errorf("%s: user-only example must pin --as user\nexample: %s", cmd, example) + } + if outbound[cmd] && !strings.Contains(example, "--as bot") { + t.Errorf("%s: outbound example must pin --as bot\nexample: %s", cmd, example) + } + } + } +} + func TestIMTipsFirstExampleCoversRequired(t *testing.T) { for _, cmd := range tipsExampleTargets { sc := shortcutByCommand(t, cmd) diff --git a/tests/cli_e2e/im/tips_examples_dryrun_test.go b/tests/cli_e2e/im/tips_examples_dryrun_test.go index 154963599..9be081922 100644 --- a/tests/cli_e2e/im/tips_examples_dryrun_test.go +++ b/tests/cli_e2e/im/tips_examples_dryrun_test.go @@ -122,11 +122,7 @@ func runFirstExampleDryRun(t *testing.T, command string, wantAPIPath string) { exampleArgs := firstExampleArgs(t, command) args := append(exampleArgs, "--dry-run") - req := clie2e.Request{Args: args, WorkDir: t.TempDir()} - if !hasAsFlag(exampleArgs) { - req.DefaultAs = "bot" - } - result, err := clie2e.RunCmd(ctx, req) + result, err := clie2e.RunCmd(ctx, clie2e.Request{Args: args, WorkDir: t.TempDir()}) require.NoError(t, err) result.AssertExitCode(t, 0) require.Contains(t, result.Stdout, wantAPIPath, @@ -159,35 +155,24 @@ var tipsExampleAllTargets = []string{ "+flag-create", "+flag-cancel", } -// defaultAsForCommand picks the identity to run the dry-run under by reading -// the shortcut's own AuthTypes: "bot" when the shortcut supports bot identity -// (matching the 3 pre-existing path-assertion tests above), otherwise "user" -// for user-only shortcuts (+messages-search and the whole feed/flag series). -func defaultAsForCommand(t *testing.T, command string) string { - t.Helper() - for _, sc := range imshortcuts.Shortcuts() { - if sc.Command != command { - continue +// asFlagValue returns the value following --as in the example, or "". +func asFlagValue(args []string) string { + for i, a := range args { + if a == "--as" && i+1 < len(args) { + return args[i+1] } - for _, a := range sc.AuthTypes { - if a == "bot" { - return "bot" - } - } - return "user" } - t.Fatalf("shortcut %s not found", command) return "" } // TestIMTipsAllExamplesDryRun extends the executability lock from the 3 // path-assertion tests above (messages-send, chat-messages-list, // resources-download) to every "Example:" tip of all 18 shortcuts: each -// example, with placeholders substituted and --dry-run appended, must exit 0. -// This only asserts exit code, not the API path — the 3 tests above keep -// that stronger assertion for their targets. Examples that already carry an -// explicit --as run verbatim; only --as-less examples get an identity -// injected (matching each shortcut's AuthTypes). +// example, with placeholders substituted and --dry-run appended, runs +// VERBATIM — no identity is injected, so the test proves the copied example +// itself is runnable, not a framework-completed variant of it. Examples that +// carry an explicit --as additionally assert the resolved identity equals +// that value. func TestIMTipsAllExamplesDryRun(t *testing.T) { for _, cmd := range tipsExampleAllTargets { for i, exampleArgs := range allExampleArgs(t, cmd) { @@ -200,16 +185,23 @@ func TestIMTipsAllExamplesDryRun(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() - req := clie2e.Request{ - Args: append(append([]string{}, exampleArgs...), "--dry-run"), + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: append(append([]string{}, exampleArgs...), "--dry-run", "--json"), WorkDir: t.TempDir(), - } - if !hasAsFlag(exampleArgs) { - req.DefaultAs = defaultAsForCommand(t, cmd) - } - result, err := clie2e.RunCmd(ctx, req) + }) require.NoError(t, err) + require.NoError(t, result.RunErr, "binary: %s args: %v", result.BinaryPath, result.Args) result.AssertExitCode(t, 0) + + if wantAs := asFlagValue(exampleArgs); wantAs != "" { + var envelope struct { + Identity string `json:"identity"` + } + require.NoError(t, json.Unmarshal([]byte(result.Stdout), &envelope), + "dry-run --json stdout should be a JSON envelope") + require.Equal(t, wantAs, envelope.Identity, + "example pins --as %s, resolved identity must match", wantAs) + } }) } }