-
Notifications
You must be signed in to change notification settings - Fork 604
feat(lark-im): support UAT for forward and add threads.forward #689
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -94,7 +94,7 @@ lark-cli im <resource> <method> [flags] # 调用 API | |
| ### messages | ||
|
|
||
| - `delete` — 撤回消息。Identity: supports `user` and `bot`; for `bot` calls, the bot must be in the chat to revoke group messages; to revoke another user's group message, the bot must be the owner, an admin, or the creator; for user P2P recalls, the target user must be within the bot's availability. | ||
| - `forward` — 转发消息。Identity: `bot` only (`tenant_access_token`). | ||
| - `forward` — 转发消息。Identity: supports `user` and `bot`. | ||
| - `merge_forward` — 合并转发消息。Identity: `bot` only (`tenant_access_token`). | ||
| - `read_users` — 查询消息已读信息。Identity: `bot` only (`tenant_access_token`); the bot must be in the chat, and can only query read status for messages it sent within the last 7 days. | ||
|
|
||
|
|
@@ -105,6 +105,10 @@ lark-cli im <resource> <method> [flags] # 调用 API | |
| - `delete` — 删除消息表情回复。Identity: supports `user` and `bot`; the caller must be in the conversation that contains the message, and can only delete reactions added by itself.[Must-read](references/lark-im-reactions.md) | ||
| - `list` — 获取消息表情回复。Identity: supports `user` and `bot`; the caller must be in the conversation that contains the message.[Must-read](references/lark-im-reactions.md) | ||
|
|
||
| ### threads | ||
|
|
||
| - `forward` — 转发话题。Identity: supports `user` and `bot`. | ||
|
Comment on lines
+108
to
+110
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Complete forward scope mapping in the permission table.
Suggested doc update shape | `messages.forward` | `im:message` |
+| `threads.forward` | `im:message` |If your actual auth mapping differs by identity, document that explicitly (e.g., user path with Also applies to: 124-147 🤖 Prompt for AI Agents |
||
|
|
||
| ### images | ||
|
|
||
| - `create` — 上传图片。Identity: `bot` only (`tenant_access_token`). | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,184 @@ | ||
| // Copyright (c) 2026 Lark Technologies Pte. Ltd. | ||
| // SPDX-License-Identifier: MIT | ||
|
|
||
| package im | ||
|
|
||
| import ( | ||
| "context" | ||
| "strings" | ||
| "testing" | ||
| "time" | ||
|
|
||
| clie2e "github.com/larksuite/cli/tests/cli_e2e" | ||
| "github.com/stretchr/testify/require" | ||
| "github.com/tidwall/gjson" | ||
| ) | ||
|
|
||
| func TestIM_MessageForwardWorkflowAsUser(t *testing.T) { | ||
| ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute) | ||
| t.Cleanup(cancel) | ||
|
|
||
| clie2e.SkipWithoutUserToken(t) | ||
|
|
||
| suffix := clie2e.GenerateSuffix() | ||
| messageText := "im-forward-msg-" + suffix | ||
| replyText := "im-forward-reply-" + suffix | ||
|
|
||
| selfOpenID := getSelfOpenID(t, ctx) | ||
| chatID, messageID := sendDirectMessageToUser(t, ctx, selfOpenID, messageText, "bot") | ||
|
|
||
|
Comment on lines
+21
to
+29
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add a bot-token precheck for bot-based fixture steps. This test uses bot identity for setup ( Suggested patch func TestIM_MessageForwardWorkflowAsUser(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
t.Cleanup(cancel)
clie2e.SkipWithoutUserToken(t)
+ clie2e.SkipWithoutBotToken(t)Also applies to: 55-63 🤖 Prompt for AI Agents |
||
| t.Run("forward message with api command as user", func(t *testing.T) { | ||
| result, err := clie2e.RunCmd(ctx, clie2e.Request{ | ||
| Args: []string{"im", "messages", "forward"}, | ||
| DefaultAs: "user", | ||
| Params: map[string]any{ | ||
| "message_id": messageID, | ||
| "receive_id_type": "chat_id", | ||
| "uuid": "msg-forward-" + suffix, | ||
| }, | ||
| Data: map[string]any{ | ||
| "receive_id": chatID, | ||
| }, | ||
| }) | ||
| require.NoError(t, err) | ||
| skipIfMissingIMForwardPermission(t, result) | ||
| result.AssertExitCode(t, 0) | ||
| result.AssertStdoutStatus(t, 0) | ||
|
|
||
| forwardedID := gjson.Get(result.Stdout, "data.message_id").String() | ||
| require.NotEmpty(t, forwardedID, "stdout:\n%s", result.Stdout) | ||
| require.NotEqual(t, messageID, forwardedID, "stdout:\n%s", result.Stdout) | ||
| require.Equal(t, chatID, gjson.Get(result.Stdout, "data.chat_id").String(), "stdout:\n%s", result.Stdout) | ||
| }) | ||
|
|
||
| var threadID string | ||
| t.Run("create thread fixture as bot", func(t *testing.T) { | ||
| result, err := clie2e.RunCmd(ctx, clie2e.Request{ | ||
| Args: []string{"im", "+messages-reply", | ||
| "--message-id", messageID, | ||
| "--text", replyText, | ||
| "--reply-in-thread", | ||
| }, | ||
| DefaultAs: "bot", | ||
| }) | ||
| require.NoError(t, err) | ||
| result.AssertExitCode(t, 0) | ||
| result.AssertStdoutStatus(t, true) | ||
|
|
||
| threadID = findThreadIDForMessage(t, ctx, chatID, messageID, "bot") | ||
| }) | ||
|
|
||
| t.Run("forward thread with api command as user", func(t *testing.T) { | ||
| result, err := clie2e.RunCmd(ctx, clie2e.Request{ | ||
| Args: []string{"im", "threads", "forward"}, | ||
| DefaultAs: "user", | ||
| Params: map[string]any{ | ||
| "thread_id": threadID, | ||
| "receive_id_type": "chat_id", | ||
| "uuid": "thread-forward-" + suffix, | ||
| }, | ||
| Data: map[string]any{ | ||
| "receive_id": chatID, | ||
| }, | ||
| }) | ||
| require.NoError(t, err) | ||
| skipIfMissingIMForwardPermission(t, result) | ||
| result.AssertExitCode(t, 0) | ||
| result.AssertStdoutStatus(t, 0) | ||
|
|
||
| forwardedID := gjson.Get(result.Stdout, "data.message_id").String() | ||
| require.NotEmpty(t, forwardedID, "stdout:\n%s", result.Stdout) | ||
| require.Equal(t, chatID, gjson.Get(result.Stdout, "data.chat_id").String(), "stdout:\n%s", result.Stdout) | ||
| require.Equal(t, "merge_forward", gjson.Get(result.Stdout, "data.msg_type").String(), "stdout:\n%s", result.Stdout) | ||
| }) | ||
| } | ||
|
Comment on lines
+17
to
+94
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add fixture cleanup to keep this live E2E test self-contained. The workflow creates source/reply/forwarded messages but does not include cleanup, so test artifacts are left behind in the target chat. As per coding guidelines: 🤖 Prompt for AI Agents |
||
|
|
||
| func findThreadIDForMessage(t *testing.T, ctx context.Context, chatID string, messageID string, defaultAs string) string { | ||
| t.Helper() | ||
|
|
||
| listResult, err := clie2e.RunCmdWithRetry(ctx, clie2e.Request{ | ||
| Args: []string{ | ||
| "im", "+chat-messages-list", | ||
| "--chat-id", chatID, | ||
| "--start", time.Now().UTC().Add(-10 * time.Minute).Format(time.RFC3339), | ||
| "--end", time.Now().UTC().Add(10 * time.Minute).Format(time.RFC3339), | ||
| }, | ||
| DefaultAs: defaultAs, | ||
| }, clie2e.RetryOptions{ | ||
| ShouldRetry: func(result *clie2e.Result) bool { | ||
| if result == nil || result.ExitCode != 0 { | ||
| return true | ||
| } | ||
| for _, item := range gjson.Get(result.Stdout, "data.messages").Array() { | ||
| if item.Get("message_id").String() == messageID && item.Get("thread_id").String() != "" { | ||
| return false | ||
| } | ||
| } | ||
| return true | ||
| }, | ||
| }) | ||
| require.NoError(t, err) | ||
| listResult.AssertExitCode(t, 0) | ||
| listResult.AssertStdoutStatus(t, true) | ||
|
|
||
| for _, item := range gjson.Get(listResult.Stdout, "data.messages").Array() { | ||
| if item.Get("message_id").String() == messageID { | ||
| threadID := item.Get("thread_id").String() | ||
| require.NotEmpty(t, threadID, "expected thread_id for message %s in stdout:\n%s", messageID, listResult.Stdout) | ||
| return threadID | ||
| } | ||
| } | ||
|
|
||
| t.Fatalf("expected message %s in stdout:\n%s", messageID, listResult.Stdout) | ||
| return "" | ||
| } | ||
|
|
||
| func getSelfOpenID(t *testing.T, ctx context.Context) string { | ||
| t.Helper() | ||
|
|
||
| result, err := clie2e.RunCmd(ctx, clie2e.Request{ | ||
| Args: []string{"contact", "+get-user"}, | ||
| DefaultAs: "user", | ||
| }) | ||
| require.NoError(t, err) | ||
| result.AssertExitCode(t, 0) | ||
| result.AssertStdoutStatus(t, true) | ||
|
|
||
| openID := gjson.Get(result.Stdout, "data.user.open_id").String() | ||
| require.NotEmpty(t, openID, "stdout:\n%s", result.Stdout) | ||
| return openID | ||
| } | ||
|
|
||
| func sendDirectMessageToUser(t *testing.T, ctx context.Context, userOpenID string, text string, defaultAs string) (string, string) { | ||
| t.Helper() | ||
|
|
||
| result, err := clie2e.RunCmd(ctx, clie2e.Request{ | ||
| Args: []string{"im", "+messages-send", | ||
| "--user-id", userOpenID, | ||
| "--text", text, | ||
| }, | ||
| DefaultAs: defaultAs, | ||
| }) | ||
| require.NoError(t, err) | ||
| result.AssertExitCode(t, 0) | ||
| result.AssertStdoutStatus(t, true) | ||
|
|
||
| chatID := gjson.Get(result.Stdout, "data.chat_id").String() | ||
| messageID := gjson.Get(result.Stdout, "data.message_id").String() | ||
| require.NotEmpty(t, chatID, "stdout:\n%s", result.Stdout) | ||
| require.NotEmpty(t, messageID, "stdout:\n%s", result.Stdout) | ||
| return chatID, messageID | ||
| } | ||
|
|
||
| func skipIfMissingIMForwardPermission(t *testing.T, result *clie2e.Result) { | ||
| t.Helper() | ||
| if result == nil || result.ExitCode == 0 { | ||
| return | ||
| } | ||
| stderrLower := strings.ToLower(result.Stderr) | ||
| if strings.Contains(stderrLower, "permission denied") || | ||
| strings.Contains(stderrLower, "230027") || | ||
| strings.Contains(stderrLower, "missing_scope") { | ||
| t.Skipf("skip UAT forward workflow due to missing IM forward permissions: %s", result.Stderr) | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: larksuite/cli
Length of output: 12646
🏁 Script executed:
sed -n '34,39p' internal/core/types_test.goRepository: larksuite/cli
Length of output: 261
🏁 Script executed:
sed -n '32,48p' internal/core/types.goRepository: larksuite/cli
Length of output: 527
Update endpoint tests to verify the default domain change to
feishu-pre.cn.The default endpoint for non-Lark brands changed from
https://open.feishu.cntohttps://open.feishu-pre.cn, but the testTestResolveEndpoints_EmptyDefaultsToFeishuininternal/core/types_test.gostill expects the old.cndomain. Update the test assertions to expect.feishu-pre.cnfor the default case.🤖 Prompt for AI Agents