Files
dingtalk-workspace-cli/internal/shortcut/chat/coverage_completion_test.go

780 lines
30 KiB
Go

// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
package chat
import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"reflect"
"strconv"
"strings"
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/helpers"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition"
)
func TestCrossPlatformCoverageConversationValidationAndTypeVariants(t *testing.T) {
fake := &larkAlignmentCaller{}
helpers.InitDeps(fake)
root := newPlatformCoverageRoot()
tooMany := make([]string, 11)
for i := range tooMany {
tooMany[i] = fmt.Sprintf("cid-%d", i)
}
root.SetArgs([]string{
"chat", "+conversation-set-top",
"--conversation-ids", strings.Join(tooMany, ","),
"--yes",
})
if err := root.Execute(); err == nil {
t.Fatal("more than ten conversation IDs were accepted")
}
root = newPlatformCoverageRoot()
root.SetArgs([]string{"chat", "+category-create", "--title", " ", "--yes"})
if err := root.Execute(); err == nil {
t.Fatal("blank category title was accepted")
}
shortcut.Register(shortcut.Shortcut{
Service: "chat",
Command: "+coverage-category-title",
Flags: []shortcut.Flag{{Name: "title", Type: shortcut.FlagString}},
Validate: validateConversationCategoryTitle,
Execute: func(*shortcut.RuntimeContext) error { return nil },
})
root = newPlatformCoverageRoot()
root.SetArgs([]string{"chat", "+coverage-category-title", "--title", " "})
if err := root.Execute(); err == nil || !strings.Contains(err.Error(), "不能为空") {
t.Fatalf("direct blank category validation error = %v", err)
}
tests := []struct {
value map[string]any
want string
}{
{map[string]any{"singleChat": "true"}, "direct"},
{map[string]any{"singleChat": "false"}, "group"},
{map[string]any{"singleChat": float64(1)}, "direct"},
{map[string]any{"singleChat": float64(0)}, "group"},
{map[string]any{"singleChat": 1}, "direct"},
{map[string]any{"singleChat": 0}, "group"},
{map[string]any{"conversationType": "group_chat"}, "group"},
}
for _, tc := range tests {
if got, ok := conversationListTopType(tc.value); !ok || got != tc.want {
t.Errorf("conversationListTopType(%#v) = %q, %v; want %q", tc.value, got, ok, tc.want)
}
}
}
func TestCrossPlatformCoverageConversationAndGroupListExecution(t *testing.T) {
fake := &larkAlignmentCaller{}
helpers.InitDeps(fake)
for _, args := range [][]string{
{"chat", "+conversation-set-top", "--conversation-id", "cid", "--yes"},
{"chat", "+conversation-list", "--limit", "1", "--cursor", "1", "--exclude-muted"},
{"chat", "+conversation-list-top", "--limit", "1", "--cursor", "1", "--exclude-muted", "--type", "group"},
{"chat", "+category-list-conversations", "--category-id", "1", "--exclude-muted"},
{"chat", "+chat-list-mine", "--role", "OWNER", "--limit", "1", "--exclude-muted"},
{"chat", "+chat-list-all", "--limit", "1", "--cursor", "next"},
{"chat", "+messages-list-pin", "--open-conversation-id", "cid", "--cursor", "next", "--size", "1"},
} {
root := newPlatformCoverageRoot()
root.SetArgs(args)
if err := root.Execute(); err != nil {
t.Fatalf("%v: %v", args, err)
}
}
}
func TestCrossPlatformCoverageConversationListFailureBoundaries(t *testing.T) {
for _, tail := range [][]string{
{"--limit", "0"},
{"--limit", "101"},
{"--page-limit", "2"},
{"--page-all", "--page-limit", "0"},
{"--page-all", "--page-limit", "501"},
} {
root := newPlatformCoverageRoot()
root.SetArgs(append([]string{"chat", "+conversation-list"}, tail...))
if err := root.Execute(); err == nil {
t.Errorf("invalid conversation args succeeded: %v", tail)
}
}
for _, tc := range []struct {
name string
caller *larkAlignmentCaller
args []string
wantError bool
}{
{name: "first read failure", caller: &larkAlignmentCaller{failProductTool: "im/list_all_conversations"}, wantError: true},
{name: "later read failure", caller: &larkAlignmentCaller{
sequenceResponses: map[string][]string{"im/list_all_conversations": {`{"result":{"conversationList":[],"hasMore":true,"nextCursor":2}}`}},
failProductToolAt: map[string]int{"im/list_all_conversations": 2},
}, args: []string{"--page-all"}},
{name: "missing pagination", caller: &larkAlignmentCaller{responses: map[string]string{"im/list_all_conversations": `{"result":{"conversationList":[]}}`}}},
{name: "invalid cursor", caller: &larkAlignmentCaller{responses: map[string]string{"im/list_all_conversations": `{"result":{"conversationList":[],"hasMore":true,"nextCursor":"bad"}}`}}},
{name: "stalled cursor", caller: &larkAlignmentCaller{responses: map[string]string{"im/list_all_conversations": `{"result":{"conversationList":[],"hasMore":true,"nextCursor":2}}`}}, args: []string{"--page-all", "--cursor", "2"}},
{name: "page limit", caller: &larkAlignmentCaller{responses: map[string]string{"im/list_all_conversations": `{"result":{"conversationList":[],"hasMore":true,"nextCursor":2}}`}}, args: []string{"--page-all", "--page-limit", "1"}},
} {
t.Run(tc.name, func(t *testing.T) {
helpers.InitDeps(tc.caller)
root := newPlatformCoverageRoot()
root.SetArgs(append([]string{"chat", "+conversation-list"}, tc.args...))
err := root.Execute()
if (err != nil) != tc.wantError {
t.Fatalf("error = %v, wantError=%v", err, tc.wantError)
}
})
}
for value, want := range map[any]int64{int(1): 1, int64(2): 2, float64(3): 3, "4": 4} {
got, err := conversationPaginationCursor(value)
if err != nil || got != want {
t.Fatalf("cursor %#v = %d, %v; want %d", value, got, err, want)
}
}
if _, err := conversationPaginationCursor(struct{}{}); err == nil {
t.Fatal("unsupported cursor unexpectedly accepted")
}
if got := unwrapConversationTuple(nil); got != nil {
t.Fatalf("empty tuple = %#v", got)
}
}
func TestCrossPlatformCoverageChatCreateAndReplyFailures(t *testing.T) {
tests := []struct {
name string
caller *larkAlignmentCaller
args []string
wantError string
}{
{
name: "owner query resolution",
caller: &larkAlignmentCaller{failProductTool: "contact/search_contact_by_key_word"},
args: []string{"chat", "+chat-create", "--name", "群", "--users", "u1", "--owner-query", "群主", "--yes"},
wantError: "fixture lower call failed",
},
{
name: "current profile call",
caller: &larkAlignmentCaller{failProductTool: "contact/get_current_user_profile"},
args: []string{"chat", "+chat-create", "--name", "群", "--users", "u1", "--yes"},
wantError: "读取当前用户",
},
{
name: "missing current user",
caller: &larkAlignmentCaller{responses: map[string]string{
"contact/get_current_user_profile": `{"result":[]}`,
}},
args: []string{"chat", "+chat-create", "--name", "群", "--users", "u1", "--yes"},
wantError: "缺少 userId",
},
{
name: "create write",
caller: &larkAlignmentCaller{failProductTool: "im/create_group_conversation"},
args: []string{"chat", "+chat-create", "--name", "群", "--users", "u1", "--yes"},
wantError: "fixture lower call failed",
},
{
name: "explicit sender lookup",
caller: &larkAlignmentCaller{failProductTool: "contact/search_contact_by_key_word"},
args: []string{"chat", "+messages-reply", "--conversation-id", "cid", "--message-id", "msg", "--ref-sender", "user-id", "--text", "收到", "--yes"},
wantError: "解析为 openDingTalkId",
},
{
name: "explicit sender unresolved",
caller: &larkAlignmentCaller{responses: map[string]string{
"contact/search_contact_by_key_word": `{"result":[]}`,
}},
args: []string{"chat", "+messages-reply", "--conversation-id", "cid", "--message-id", "msg", "--ref-sender", "user-id", "--text", "收到", "--yes"},
wantError: "没有精确匹配",
},
{
name: "referenced message lookup",
caller: &larkAlignmentCaller{failProductTool: "im/list_messages_by_ids"},
args: []string{"chat", "+messages-reply", "--conversation-id", "cid", "--message-id", "msg", "--text", "收到", "--yes"},
wantError: "读取被引用消息",
},
{
name: "referenced message missing sender",
caller: &larkAlignmentCaller{responses: map[string]string{
"im/list_messages_by_ids": `{"result":[{"openMessageId":"other","senderOpenDingTalkId":"D-other"},{"openMessageId":"msg"}]}`,
}},
args: []string{"chat", "+messages-reply", "--conversation-id", "cid", "--message-id", "msg", "--text", "收到", "--yes"},
wantError: "未返回 senderOpenDingTalkId",
},
{
name: "feed source",
caller: &larkAlignmentCaller{failProductTool: "im/list_conversations_by_category"},
args: []string{"chat", "+feed-group-query-item", "--category-id", "1", "--conversation-ids", "cid"},
wantError: "fixture lower call failed",
},
{
name: "chat update resolution",
caller: &larkAlignmentCaller{responses: map[string]string{"im/search_groups": `{"result":[]}`}},
args: []string{"chat", "+chat-update", "--group", "missing", "--name", "新群", "--yes"},
wantError: "没有找到",
},
{
name: "reply write",
caller: &larkAlignmentCaller{failProductTool: "chat/send_personal_message"},
args: []string{"chat", "+messages-reply", "--conversation-id", "cid", "--message-id", "msg", "--ref-sender", fixtureCurrentDOpenID, "--text", "收到", "--yes"},
wantError: "fixture lower call failed",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
helpers.InitDeps(tc.caller)
root := newPlatformCoverageRoot()
root.SetArgs(tc.args)
err := root.Execute()
if err == nil || !strings.Contains(err.Error(), tc.wantError) {
t.Fatalf("error = %v, want containing %q", err, tc.wantError)
}
})
}
dry := &larkAlignmentCaller{}
helpers.InitDeps(dry)
root := newPlatformCoverageRoot()
root.SetArgs([]string{"chat", "+chat-create", "--name", "群", "--users", "u1", "--dry-run", "--yes"})
if err := root.Execute(); err != nil {
t.Fatal(err)
}
if len(dry.calls) != 2 ||
dry.calls[0].tool != "get_current_user_profile" ||
dry.calls[1].tool != "create_group_conversation" {
t.Fatalf("chat-create dry-run calls = %#v", dry.calls)
}
external := &larkAlignmentCaller{responses: map[string]string{
"contact/search_contact_by_key_word": `{"result":[{"name":"外部联系人","openDingTalkId":"D-external"}]}`,
}}
helpers.InitDeps(external)
root = newPlatformCoverageRoot()
root.SetArgs([]string{"chat", "+chat-create", "--name", "群", "--owner-open-dingtalk-id", fixtureCurrentDOpenID, "--member-query", "外部联系人", "--yes"})
if err := root.Execute(); err != nil {
t.Fatal(err)
}
}
func TestCrossPlatformCoverageNaturalGroupReadFailures(t *testing.T) {
for _, command := range []string{"+chat-invite-url", "+chat-bots"} {
helpers.InitDeps(&larkAlignmentCaller{responses: map[string]string{"im/search_groups": `{"result":[]}`}})
root := newPlatformCoverageRoot()
root.SetArgs([]string{"chat", command, "--group", "missing"})
if err := root.Execute(); err == nil {
t.Errorf("%s missing natural group unexpectedly resolved", command)
}
}
}
func TestCrossPlatformCoverageReplyShapeHelpers(t *testing.T) {
if got := findOpenDingTalkID([]map[string]any{{"openDingTalkId": "D-map"}}); got != "D-map" {
t.Fatalf("findOpenDingTalkID([]map) = %q", got)
}
if got := shortcutMessageMaps(nil); got != nil {
t.Fatalf("nil message maps = %#v", got)
}
if got := shortcutMessageMaps(map[string]any{}); got != nil {
t.Fatalf("empty message maps = %#v", got)
}
maps := shortcutMessageMaps(map[string]any{
"data": map[string]any{
"items": []map[string]any{{"openMessageId": "msg"}},
},
})
if len(maps) != 1 || maps[0]["openMessageId"] != "msg" {
t.Fatalf("nested []map message maps = %#v", maps)
}
if got := currentProfileUserID(map[string]any{"result": map[string]any{"userId": "nested"}}); got != "nested" {
t.Fatalf("nested current profile = %q", got)
}
if got := currentProfileUserID(map[string]any{"userId": "direct"}); got != "direct" {
t.Fatalf("direct current profile = %q", got)
}
data := map[string]any{"result": "not-a-map"}
normalizeCreatedConversation(data)
if data["result"] != "not-a-map" {
t.Fatalf("normalization changed non-map result: %#v", data)
}
}
func TestCrossPlatformCoverageFlagAndMgetValidation(t *testing.T) {
tooMany := make([]string, 11)
for i := range tooMany {
tooMany[i] = fmt.Sprintf("msg-%d", i)
}
cases := [][]string{
{"chat", "+flag-create", "--message-ids", strings.Join(tooMany, ","), "--conversation-id", "cid", "--yes"},
{"chat", "+flag-list", "--cursor", "-1"},
{"chat", "+flag-list", "--size", "31"},
{"chat", "+messages-mget", "--msg-ids", strings.Join(makeIDs(51), ",")},
{"chat", "+messages-mget", "--msg-ids", "msg", "--download-resources", "--output-dir", "../escape"},
}
for _, args := range cases {
root := newPlatformCoverageRoot()
root.SetArgs(args)
if err := root.Execute(); err == nil {
t.Errorf("invalid args unexpectedly succeeded: %v", args)
}
}
fake := &larkAlignmentCaller{failProductTool: "im/list_messages_by_ids"}
helpers.InitDeps(fake)
root := newPlatformCoverageRoot()
root.SetArgs([]string{"chat", "+messages-mget", "--msg-ids", "msg", "--yes"})
if err := root.Execute(); err == nil {
t.Fatal("mget lower error was swallowed")
}
}
func TestCrossPlatformCoverageRecallCardAndLedgerBoundaries(t *testing.T) {
for _, tc := range []struct {
name string
caller *larkAlignmentCaller
args []string
}{
{name: "multiple recall ids", caller: &larkAlignmentCaller{}, args: []string{"chat", "+messages-recall", "--message-ids", "m1,m2", "--yes"}},
{name: "recall lookup failure", caller: &larkAlignmentCaller{failProductTool: "im/list_messages_by_ids"}, args: []string{"chat", "+messages-recall", "--msg-id", "m1", "--yes"}},
{name: "recall lookup empty", caller: &larkAlignmentCaller{responses: map[string]string{"im/list_messages_by_ids": `{"result":[]}`}}, args: []string{"chat", "+messages-recall", "--msg-id", "m1", "--yes"}},
{name: "recall lookup missing conversation", caller: &larkAlignmentCaller{responses: map[string]string{"im/list_messages_by_ids": `{"result":[{"openMessageId":"m1"}]}`}}, args: []string{"chat", "+messages-recall", "--msg-id", "m1", "--yes"}},
{name: "card status low", caller: &larkAlignmentCaller{}, args: []string{"chat", "+messages-update-card", "--biz-id", "b", "--content", "x", "--flow-status", "0", "--yes"}},
{name: "card status high", caller: &larkAlignmentCaller{}, args: []string{"chat", "+messages-update-card", "--biz-id", "b", "--content", "x", "--flow-status", "6", "--yes"}},
} {
t.Run(tc.name, func(t *testing.T) {
helpers.InitDeps(tc.caller)
root := newPlatformCoverageRoot()
root.SetArgs(tc.args)
if err := root.Execute(); err == nil {
t.Fatal("failure boundary unexpectedly succeeded")
}
})
}
helpers.InitDeps(&larkAlignmentCaller{responses: map[string]string{
"im/update_streaming_card": `{"result":{"updated":true}}`,
}})
root := newPlatformCoverageRoot()
root.SetArgs([]string{"chat", "+messages-update-card", "--biz-id", "b", "--content", "x", "--flow-status", "3", "--yes"})
if err := root.Execute(); err != nil {
t.Fatal(err)
}
for value, want := range map[any]int{
int(1): 1, int32(2): 2, int64(3): 3, float32(4): 4, float64(5): 5, "bad": 0,
} {
if got := messageLedgerInt(value); got != want {
t.Fatalf("messageLedgerInt(%#v) = %d, want %d", value, got, want)
}
}
}
func TestCrossPlatformCoverageFeedCompleteAndExcludeMuted(t *testing.T) {
fake := &larkAlignmentCaller{category: `{"result":{"hasMore":false,"list":[{"openConversationId":"cid"}]}}`}
helpers.InitDeps(fake)
root := newPlatformCoverageRoot()
root.SetArgs([]string{
"chat", "+feed-group-query-item",
"--category-id", "1",
"--conversation-ids", "cid",
"--exclude-muted",
})
if err := root.Execute(); err != nil {
t.Fatal(err)
}
if len(fake.calls) != 1 || fake.calls[0].args["excludeMuted"] != true {
t.Fatalf("feed calls = %#v", fake.calls)
}
}
func TestCrossPlatformCoverageUnifiedSendValidationMatrix(t *testing.T) {
cases := [][]string{
{"--identity", "user", "--group", "cid", "--users", "u1", "--text", "x"},
{"--identity", "user", "--text", "x"},
{"--identity", "user", "--group", "cid", "--open-dingtalk-id", fixtureCurrentDOpenID, "--text", "x"},
{"--identity", "user", "--group", "cid", "--at-user-ids", "u1", "--text", "x"},
{"--identity", "user", "--open-dingtalk-id", fixtureCurrentDOpenID, "--at-all", "--text", "x"},
{"--identity", "bot", "--group", "cid", "--text", "x"},
{"--identity", "bot", "--robot-code", "r", "--group", "cid", "--users", "u1", "--text", "x"},
{"--identity", "bot", "--robot-code", "r", "--group", "cid", "--open-dingtalk-id", fixtureCurrentDOpenID, "--text", "x"},
{"--identity", "bot", "--robot-code", "r", "--group", "cid", "--at-mobiles", "13800000000", "--text", "x"},
{"--identity", "bot", "--robot-code", "r", "--users", "u1", "--at-user-ids", "u2", "--text", "x"},
{"--identity", "webhook", "--text", "x"},
{"--identity", "webhook", "--webhook-token", "token", "--group", "cid", "--text", "x"},
{"--identity", "webhook", "--webhook-token", "token", "--at-open-dingtalk-ids", fixtureCurrentDOpenID, "--text", "x"},
{"--identity", "webhook", "--webhook-token", "token", "--uuid", "key", "--text", "x"},
}
for _, tail := range cases {
fake := &larkAlignmentCaller{}
helpers.InitDeps(fake)
root := newPlatformCoverageRoot()
args := append([]string{"chat", "+messages-send"}, tail...)
args = append(args, "--yes")
root.SetArgs(args)
if err := root.Execute(); err == nil {
t.Errorf("invalid unified send unexpectedly succeeded: %v", tail)
}
if len(fake.calls) != 0 {
t.Errorf("invalid unified send reached lower service: %v => %#v", tail, fake.calls)
}
}
}
func TestCrossPlatformCoverageUnifiedSendOptionalArgumentsAndErrors(t *testing.T) {
tests := []struct {
name string
args []string
want map[string]any
}{
{
name: "user group mentions",
args: []string{"--identity", "user", "--group", "cid", "--text", "@" + fixtureCurrentDOpenID + " <@" + fixtureCurrentDOpenID2 + "> x", "--at-open-dingtalk-ids", fixtureCurrentDOpenID + "," + fixtureCurrentDOpenID2, "--at-all"},
want: map[string]any{"atOpenDingTalkIds": []string{fixtureCurrentDOpenID, fixtureCurrentDOpenID2}, "atAll": true},
},
{
name: "user direct",
args: []string{"--identity", "user", "--open-dingtalk-id", fixtureCurrentDOpenID, "--text", "x"},
want: map[string]any{"receiverOpenDingTalkId": fixtureCurrentDOpenID},
},
{
name: "bot group mentions",
args: []string{"--identity", "bot", "--robot-code", "r", "--group", "cid", "--text", "x", "--at-user-ids", "u1", "--at-open-dingtalk-ids", fixtureCurrentDOpenID},
want: map[string]any{"atUserIds": []string{"u1"}, "atOpendingtalkIds": []string{fixtureCurrentDOpenID}},
},
{
name: "bot direct targets",
args: []string{"--identity", "bot", "--robot-code", "r", "--users", "u1", "--open-dingtalk-ids", fixtureCurrentDOpenID, "--text", "x", "--at-all"},
want: map[string]any{"userIds": []string{"u1"}, "openDingtalkIds": []string{fixtureCurrentDOpenID}, "isAtAll": "true"},
},
{
name: "webhook mentions",
args: []string{"--identity", "webhook", "--webhook-token", "token", "--text", "x", "--at-user-ids", "u1", "--at-mobiles", "13800000000"},
want: map[string]any{"atUserIds": []string{"u1"}, "atMobiles": []string{"13800000000"}},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
fake := &larkAlignmentCaller{}
helpers.InitDeps(fake)
root := newPlatformCoverageRoot()
args := append([]string{"chat", "+messages-send"}, tc.args...)
args = append(args, "--yes")
root.SetArgs(args)
if err := root.Execute(); err != nil {
t.Fatal(err)
}
call := fake.calls[len(fake.calls)-1]
for key, want := range tc.want {
if !reflect.DeepEqual(call.args[key], want) {
t.Errorf("%s = %#v, want %#v", key, call.args[key], want)
}
}
})
}
fake := &larkAlignmentCaller{}
helpers.InitDeps(fake)
root := newPlatformCoverageRoot()
root.SetArgs([]string{"chat", "+messages-send", "--identity", "user", "--group", "cid", "--text", "x", "--dry-run", "--yes"})
if err := root.Execute(); err != nil {
t.Fatal(err)
}
if len(fake.calls) != 0 {
t.Fatalf("unified send dry-run reached lower service: %#v", fake.calls)
}
fake = &larkAlignmentCaller{failProductTool: "chat/send_personal_message"}
helpers.InitDeps(fake)
root = newPlatformCoverageRoot()
root.SetArgs([]string{"chat", "+messages-send", "--identity", "user", "--group", "cid", "--text", "x", "--yes"})
if err := root.Execute(); err == nil {
t.Fatal("unified send write error was swallowed")
}
if got := shortcutMessageTitle(strings.Repeat("界", 45)); len([]rune(got)) != 40 {
t.Fatalf("long generated title has %d runes", len([]rune(got)))
}
}
func TestCrossPlatformCoverageMessagesSendRejectsMismatchedCurrentUserMentions(t *testing.T) {
tests := []struct {
name string
body string
atIDs string
atAll bool
wantErr string
}{
{
name: "declared member missing from body",
body: "请查收",
atIDs: fixtureCurrentDOpenID,
wantErr: "必须在正文中使用对应",
},
{
name: "declared member only appears as a longer token prefix",
body: "@" + fixtureCurrentDOpenID + "_suffix 请查收",
atIDs: fixtureCurrentDOpenID,
wantErr: "必须在正文中使用对应",
},
{
name: "body member missing from declaration",
body: "<@" + fixtureCurrentDOpenID + "> 请查收",
wantErr: "必须同时通过 --at-open-dingtalk-ids 声明",
},
{
name: "body at-all missing from declaration",
body: "<@all> 请查收",
wantErr: "必须同时指定 --at-all",
},
{
name: "bare body member missing from declaration",
body: "@" + fixtureCurrentDOpenID + " 请查收",
wantErr: "必须同时通过 --at-open-dingtalk-ids 声明",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fake := &larkAlignmentCaller{}
helpers.InitDeps(fake)
root := newPlatformCoverageRoot()
args := []string{
"chat", "+messages-send", "--identity", "user",
"--group", "cid", "--markdown", tt.body, "--yes",
}
if tt.atIDs != "" {
args = append(args, "--at-open-dingtalk-ids", tt.atIDs)
}
if tt.atAll {
args = append(args, "--at-all")
}
root.SetArgs(args)
err := root.Execute()
if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
t.Fatalf("error = %v, want %q", err, tt.wantErr)
}
if len(fake.calls) != 0 {
t.Fatalf("invalid mention contract reached lower service: %#v", fake.calls)
}
})
}
}
func TestCrossPlatformCoverageUnifiedSendUnsupportedIdentityGuard(t *testing.T) {
flags := append([]shortcut.Flag(nil), MessagesSend.Flags...)
for i := range flags {
if flags[i].Name == "identity" || flags[i].Name == "as" {
flags[i].Enum = nil
}
}
shortcut.Register(shortcut.Shortcut{
Service: "chat",
Command: "+coverage-unified-send",
Flags: flags,
Execute: executeMessagesSend,
})
root := newPlatformCoverageRoot()
root.SetArgs([]string{"chat", "+coverage-unified-send", "--identity", "unsupported", "--text", "x"})
err := root.Execute()
if err == nil || !strings.Contains(err.Error(), "unsupported identity") {
t.Fatalf("unsupported identity error = %v", err)
}
}
func TestCrossPlatformCoverageUnifiedSendGroupFileAndBatchBoundaries(t *testing.T) {
readGroupFile := messagesSendReadGroupFile
t.Cleanup(func() { messagesSendReadGroupFile = readGroupFile })
for _, tail := range [][]string{
{"--identity", "bot", "--robot-code", "r", "--group", "cid", "--groups", "c1", "--text", "x"},
{"--identity", "bot", "--robot-code", "r", "--groups", "c1", "--groups-file", "groups.txt", "--text", "x"},
{"--identity", "webhook", "--webhook-token", "token", "--chat-query", "群", "--text", "x"},
{"--identity", "bot", "--robot-code", "r", "--groups", "", "--text", "x"},
{"--identity", "bot", "--robot-code", "r", "--groups", strings.Join(makeIDs(101), ","), "--text", "x"},
{"--identity", "bot", "--robot-code", "r", "--groups-file", "/absolute.txt", "--text", "x"},
{"--identity", "bot", "--robot-code", "r", "--groups-file", "../escape.txt", "--text", "x"},
{"--identity", "bot", "--robot-code", "r", "--groups-file", "missing.txt", "--text", "x"},
} {
helpers.InitDeps(&larkAlignmentCaller{})
root := newPlatformCoverageRoot()
root.SetArgs(append([]string{"chat", "+messages-send"}, append(tail, "--yes")...))
if err := root.Execute(); err == nil {
t.Errorf("invalid unified send args succeeded: %v", tail)
}
}
temp := t.TempDir()
if err := os.Mkdir(filepath.Join(temp, "groups-dir"), 0o700); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(temp, "large.txt"), make([]byte, messagesSendMaxGroupFileSize+1), 0o600); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(temp, "unreadable.txt"), []byte("cid"), 0o600); err != nil {
t.Fatal(err)
}
messagesSendReadGroupFile = func(path string) ([]byte, error) {
if filepath.Base(path) == "unreadable.txt" {
return nil, os.ErrPermission
}
return readGroupFile(path)
}
t.Chdir(temp)
for _, path := range []string{"groups-dir", "large.txt", "unreadable.txt"} {
helpers.InitDeps(&larkAlignmentCaller{})
root := newPlatformCoverageRoot()
root.SetArgs([]string{"chat", "+messages-send", "--identity", "bot", "--robot-code", "r", "--groups-file", path, "--text", "x", "--yes"})
if err := root.Execute(); err == nil {
t.Errorf("invalid group file %q succeeded", path)
}
}
fake := &larkAlignmentCaller{}
helpers.InitDeps(fake)
root := newPlatformCoverageRoot()
root.SetArgs([]string{
"chat", "+messages-send", "--identity", "bot", "--robot-code", "r",
"--groups", "c1,c2", "--text", "x", "--at-user-ids", "u1",
"--at-open-dingtalk-ids", fixtureCurrentDOpenID, "--at-all", "--yes",
})
if err := root.Execute(); err != nil {
t.Fatal(err)
}
if len(fake.calls) != 2 {
t.Fatalf("batch calls = %#v", fake.calls)
}
for _, call := range fake.calls {
if !reflect.DeepEqual(call.args["atUserIds"], []string{"u1"}) ||
!reflect.DeepEqual(call.args["atOpendingtalkIds"], []string{fixtureCurrentDOpenID}) ||
call.args["isAtAll"] != "true" {
t.Fatalf("batch mention args = %#v", call.args)
}
}
shortcut.Register(shortcut.Shortcut{
Service: "chat", Command: "+coverage-unified-execute-error",
Flags: MessagesSend.Flags, Execute: executeMessagesSend,
})
root = newPlatformCoverageRoot()
root.SetArgs([]string{
"chat", "+coverage-unified-execute-error", "--identity", "bot", "--robot-code", "r",
"--groups-file", "missing-again.txt", "--text", "x",
})
if err := root.Execute(); err == nil {
t.Fatal("execute-time group file error was swallowed")
}
}
func TestCrossPlatformCoverageMuteMemberResolutionFailures(t *testing.T) {
tests := []struct {
name string
caller *muteMemberScenarioCaller
}{
{"contact error", &muteMemberScenarioCaller{contactErr: errors.New("contact failed")}},
{"contact missing name", &muteMemberScenarioCaller{contactText: `{"result":[]}`}},
{"member error", &muteMemberScenarioCaller{memberErr: errors.New("members failed")}},
{"member missing fields", &muteMemberScenarioCaller{memberMode: "missing-fields"}},
{"member cursor missing", &muteMemberScenarioCaller{memberMode: "missing-cursor"}},
{"member ambiguous", &muteMemberScenarioCaller{memberMode: "ambiguous"}},
{"member page limit", &muteMemberScenarioCaller{memberMode: "page-limit"}},
{"unrelated directory result", &muteMemberScenarioCaller{
contactText: `{"result":[{"orgEmployeeModel":{"orgUserId":"other","orgUserName":"其他"}},{"orgEmployeeModel":{"orgUserId":"user-1","orgUserName":"测试成员"}}]}`,
}},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
helpers.InitDeps(tc.caller)
root := newPlatformCoverageRoot()
root.SetArgs([]string{"chat", "+chat-mute-member", "--group", "cid", "--users", "user-1", "--off", "--yes"})
if err := root.Execute(); err == nil {
t.Fatal("resolution failure scenario unexpectedly succeeded")
}
})
}
for _, tc := range []struct {
value any
want string
}{
{float64(1.5), "1.5"},
{int(2), "2"},
{int64(3), "3"},
} {
if got := shortcutString(map[string]any{"value": tc.value}, "value"); got != tc.want {
t.Errorf("shortcutString(%T) = %q, want %q", tc.value, got, tc.want)
}
}
}
type muteMemberScenarioCaller struct {
contactErr error
memberErr error
contactText string
memberMode string
}
func (c *muteMemberScenarioCaller) CallTool(
_ context.Context,
product, tool string,
args map[string]any,
) (*edition.ToolResult, error) {
switch product + "/" + tool {
case "contact/get_user_info_by_user_ids":
if c.contactErr != nil {
return nil, c.contactErr
}
text := c.contactText
if text == "" {
text = `{"result":[{"orgEmployeeModel":{"orgUserId":"user-1","orgUserName":"测试成员"}}]}`
}
return textResult(text), nil
case "chat/get_group_members":
if c.memberErr != nil {
return nil, c.memberErr
}
switch c.memberMode {
case "missing-fields":
return textResult(`{"result":{"hasMore":false,"list":[{"memberEmpName":"","openDingtalkId":""}]}}`), nil
case "missing-cursor":
return textResult(`{"result":{"hasMore":true,"list":[{"memberEmpName":"测试成员","openDingtalkId":"D1"}]}}`), nil
case "ambiguous":
return textResult(`{"result":{"hasMore":false,"list":[{"memberEmpName":"测试成员","openDingtalkId":"D1"},{"memberEmpName":"测试成员","openDingtalkId":"D2"}]}}`), nil
case "page-limit":
cursor, _ := strconv.Atoi(fmt.Sprint(args["cursor"]))
return textResult(fmt.Sprintf(
`{"result":{"hasMore":true,"nextCursor":"%d","list":[{"memberEmpName":"测试成员","openDingtalkId":"D1"}]}}`,
cursor+1,
)), nil
default:
return textResult(`{"result":{"hasMore":false,"list":[]}}`), nil
}
default:
return textResult(`{"success":true}`), nil
}
}
func (*muteMemberScenarioCaller) Format() string { return "json" }
func (*muteMemberScenarioCaller) DryRun() bool { return false }
func (*muteMemberScenarioCaller) Fields() string { return "" }
func (*muteMemberScenarioCaller) JQ() string { return "" }
func textResult(text string) *edition.ToolResult {
return &edition.ToolResult{Content: []edition.ContentBlock{{Type: "text", Text: text}}}
}
func makeIDs(count int) []string {
ids := make([]string, count)
for i := range ids {
ids[i] = fmt.Sprintf("id-%d", i)
}
return ids
}