mirror of
https://github.com/larksuite/cli.git
synced 2026-08-03 08:32:46 +08:00
fix(im): pin explicit identity on user-only examples and run them verbatim
This commit is contained in:
@@ -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 <feed_group_id>`,
|
||||
`Example: lark-cli im +feed-group-list-item --feed-group-id <feed_group_id> --as user`,
|
||||
},
|
||||
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
||||
return validateFeedGroupListOptions(runtime)
|
||||
|
||||
@@ -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_group_id> --feed-id <chat_id>`,
|
||||
`Example: lark-cli im +feed-group-query-item --feed-group-id <feed_group_id> --feed-id <chat_id> --as user`,
|
||||
},
|
||||
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
||||
_, err := buildFeedGroupQueryItemBody(runtime)
|
||||
|
||||
@@ -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 <chat_id>`,
|
||||
`Example: lark-cli im +feed-shortcut-create --chat-id <chat_id> --tail`,
|
||||
`Example: lark-cli im +feed-shortcut-create --chat-id <chat_id> --as user`,
|
||||
`Example: lark-cli im +feed-shortcut-create --chat-id <chat_id> --tail --as user`,
|
||||
},
|
||||
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
||||
if _, err := collectChatIDs(runtime); err != nil {
|
||||
|
||||
@@ -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 <chat_id1>,<chat_id2>`,
|
||||
`Example: lark-cli im +feed-shortcut-remove --chat-id <chat_id1>,<chat_id2> --as user`,
|
||||
},
|
||||
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
||||
_, err := collectChatIDs(runtime)
|
||||
|
||||
@@ -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 <message_id>`,
|
||||
`Example: lark-cli im +flag-cancel --message-id <message_id> --as user`,
|
||||
},
|
||||
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
||||
_, _, err := buildCancelItemsForPreview(runtime)
|
||||
|
||||
@@ -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 <message_id>`,
|
||||
`Example: lark-cli im +flag-create --message-id <message_id> --flag-type feed`,
|
||||
`Example: lark-cli im +flag-create --message-id <message_id> --as user`,
|
||||
`Example: lark-cli im +flag-create --message-id <message_id> --flag-type feed --as user`,
|
||||
},
|
||||
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
||||
_, err := buildCreateItemForPreview(runtime)
|
||||
|
||||
@@ -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 <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 <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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user