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

896 lines
29 KiB
Go

// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
package chat
import (
"bytes"
"context"
"encoding/json"
"errors"
"reflect"
"strings"
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/helpers"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition"
)
type larkAlignmentCall struct {
product string
tool string
args map[string]any
}
type larkAlignmentCaller struct {
calls []larkAlignmentCall
dryRun bool
failTarget string
failProductTool string
failProductToolAt map[string]int
callCounts map[string]int
category string
responses map[string]string
sequenceResponses map[string][]string
}
func (f *larkAlignmentCaller) CallTool(_ context.Context, product, tool string, args map[string]any) (*edition.ToolResult, error) {
f.calls = append(f.calls, larkAlignmentCall{product: product, tool: tool, args: args})
if f.failTarget != "" && args["openMessageId"] == f.failTarget {
return nil, errors.New("fixture write failed")
}
key := product + "/" + tool
if f.callCounts == nil {
f.callCounts = map[string]int{}
}
f.callCounts[key]++
if f.failProductTool == key {
return nil, errors.New("fixture lower call failed")
}
if f.failProductToolAt[key] == f.callCounts[key] {
return nil, errors.New("fixture sequenced lower call failed")
}
text := `{"success":true}`
switch key {
case "contact/get_current_user_profile":
text = `{"result":[{"orgEmployeeModel":{"userId":"self-user"}}]}`
case "contact/get_user_info_by_user_ids":
text = `{"result":[{"orgEmployeeModel":{"orgUserId":"user-id","orgUserName":"Resolved User"}}]}`
case "contact/search_contact_by_key_word":
keyword, _ := args["keyword"].(string)
payload, _ := json.Marshal(map[string]any{
"result": []map[string]any{
{
"name": "Fuzzy Neighbor",
"userId": keyword + "-other",
"openDingTalkId": "D-other",
},
{
"name": "Resolved User",
"userId": keyword,
"openDingTalkId": "D-resolved",
},
},
})
text = string(payload)
case "im/create_group_conversation":
text = `{"result":{"cid":"internal-cid","openCid":"open-cid"}}`
case "im/create_and_send_card":
text = `{"result":{"bizId":"biz-created"}}`
case "im/list_messages_by_ids":
text = `{"result":[{"openMessageId":"msg","openConversationId":"cid","senderOpenDingTalkId":"D-inferred","content":"{\"mediaId\":\"@image\"}"}]}`
case "im/list_conversations_by_category":
text = f.category
if text == "" {
text = `{"result":{"hasMore":false,"list":[{"openConversationId":"cid-a","conversationName":"A"},{"openConversationId":"cid-b","conversationName":"B"}]}}`
}
}
if response, ok := f.responses[key]; ok {
text = response
}
if responses := f.sequenceResponses[key]; len(responses) > 0 {
text = responses[0]
f.sequenceResponses[key] = responses[1:]
}
return &edition.ToolResult{Content: []edition.ContentBlock{{Type: "text", Text: text}}}, nil
}
func (f *larkAlignmentCaller) CallReadTool(ctx context.Context, product, tool string, args map[string]any) (*edition.ToolResult, error) {
return f.CallTool(ctx, product, tool, args)
}
func (f *larkAlignmentCaller) Format() string { return "json" }
func (f *larkAlignmentCaller) DryRun() bool { return f.dryRun }
func (f *larkAlignmentCaller) Fields() string { return "" }
func (f *larkAlignmentCaller) JQ() string { return "" }
func TestCrossPlatformCoverageEvaluationRegressionNaturalGroupTargetsAndRecallInference(t *testing.T) {
t.Run("group name to bots", func(t *testing.T) {
fake := &larkAlignmentCaller{responses: map[string]string{
"im/search_groups": `{"result":[{"openConversationId":"cid-project","title":"项目群"}],"hasMore":false}`,
"bot/list_group_bots": `{"result":{"bots":[]}}`,
}}
helpers.InitDeps(fake)
root := newPlatformCoverageRoot()
root.SetArgs([]string{"chat", "+chat-bots", "--group", "项目群"})
if err := root.Execute(); err != nil {
t.Fatal(err)
}
if len(fake.calls) != 2 || fake.calls[1].tool != "list_group_bots" || fake.calls[1].args["openConversationId"] != "cid-project" {
t.Fatalf("calls = %#v", fake.calls)
}
})
t.Run("group query to invite url", func(t *testing.T) {
fake := &larkAlignmentCaller{responses: map[string]string{
"im/search_groups": `{"result":[{"openConversationId":"cid-project","title":"项目群"}],"hasMore":false}`,
}}
helpers.InitDeps(fake)
root := newPlatformCoverageRoot()
root.SetArgs([]string{"chat", "+chat-invite-url", "--chat-query", "项目群"})
if err := root.Execute(); err != nil {
t.Fatal(err)
}
if len(fake.calls) != 2 || fake.calls[1].tool != "get_group_invite_url" || fake.calls[1].args["openConversationId"] != "cid-project" {
t.Fatalf("calls = %#v", fake.calls)
}
})
t.Run("stable id in group query bypasses search", func(t *testing.T) {
fake := &larkAlignmentCaller{}
helpers.InitDeps(fake)
root := newPlatformCoverageRoot()
root.SetArgs([]string{"chat", "+chat-invite-url", "--chat-query", "cid-fixture-chat-0001"})
if err := root.Execute(); err != nil {
t.Fatal(err)
}
if len(fake.calls) != 1 || fake.calls[0].tool != "get_group_invite_url" ||
fake.calls[0].args["openConversationId"] != "cid-fixture-chat-0001" {
t.Fatalf("calls = %#v", fake.calls)
}
})
t.Run("message id fills conversation before recall", func(t *testing.T) {
fake := &larkAlignmentCaller{}
helpers.InitDeps(fake)
root := newPlatformCoverageRoot()
root.SetArgs([]string{"chat", "+messages-recall", "--message-ids", "msg", "--yes"})
if err := root.Execute(); err != nil {
t.Fatal(err)
}
if len(fake.calls) != 2 || fake.calls[0].tool != "list_messages_by_ids" || fake.calls[1].tool != "recall_message" || fake.calls[1].args["openConversationId"] != "cid" {
t.Fatalf("calls = %#v", fake.calls)
}
})
}
func TestCrossPlatformCoverageChatCreateAddsCurrentUserAndNormalizesResult(t *testing.T) {
fake := &larkAlignmentCaller{}
helpers.InitDeps(fake)
root := newPlatformCoverageRoot()
root.SetArgs([]string{
"chat", "+chat-create",
"--name", "测试群",
"--users", "other-user,self-user",
"--type", "NORMAL",
"--thread",
"--yes",
})
if err := root.Execute(); err != nil {
t.Fatal(err)
}
if len(fake.calls) != 2 {
t.Fatalf("calls = %#v, want profile + create", fake.calls)
}
create := fake.calls[1]
if create.product != "im" || create.tool != "create_group_conversation" {
t.Fatalf("create call = %s/%s", create.product, create.tool)
}
if got, want := create.args["groupMembers"], []string{"self-user", "other-user"}; !reflect.DeepEqual(got, want) {
t.Fatalf("groupMembers = %#v, want %#v", got, want)
}
if create.args["groupType"] != "NORMAL" || create.args["convThreadEnabled"] != true {
t.Fatalf("create args = %#v", create.args)
}
payload := map[string]any{"result": map[string]any{"cid": "secret", "openCid": "open"}}
normalizeCreatedConversation(payload)
result := payload["result"].(map[string]any)
if result["openConversationId"] != "open" {
t.Fatalf("normalized result = %#v", result)
}
if _, ok := result["cid"]; ok {
t.Fatalf("internal cid leaked: %#v", result)
}
}
func TestCrossPlatformCoverageChatCreateResolvesEveryNaturalMemberBeforeCreating(t *testing.T) {
fake := &larkAlignmentCaller{responses: map[string]string{
"contact/search_contact_by_key_word": `{"result":[{"name":"张三","userId":"resolved-user","openDingTalkId":"D-resolved"}]}`,
}}
helpers.InitDeps(fake)
root := newPlatformCoverageRoot()
root.SetArgs([]string{
"chat", "+chat-create",
"--name", "测试群",
"--users", "explicit-user",
"--member-query", "张三",
"--yes",
})
if err := root.Execute(); err != nil {
t.Fatal(err)
}
if len(fake.calls) != 3 {
t.Fatalf("calls = %#v, want member resolve + current profile + create", fake.calls)
}
if fake.calls[0].tool != "search_contact_by_key_word" ||
fake.calls[1].tool != "get_current_user_profile" ||
fake.calls[2].tool != "create_group_conversation" {
t.Fatalf("call order = %#v", fake.calls)
}
if got, want := fake.calls[2].args["groupMembers"], []string{"self-user", "explicit-user", "resolved-user"}; !reflect.DeepEqual(got, want) {
t.Fatalf("groupMembers = %#v, want %#v", got, want)
}
}
func TestCrossPlatformCoverageChatCreateNaturalMemberAmbiguityStopsBeforeProfileAndCreate(t *testing.T) {
fake := &larkAlignmentCaller{responses: map[string]string{
"contact/search_contact_by_key_word": `{"result":[{"name":"张三","userId":"u1"},{"name":"张三","userId":"u2"}]}`,
}}
helpers.InitDeps(fake)
root := newPlatformCoverageRoot()
root.SetArgs([]string{
"chat", "+chat-create",
"--name", "测试群",
"--member-query", "张三",
"--yes",
})
if err := root.Execute(); err == nil {
t.Fatal("ambiguous member unexpectedly created a group")
}
if len(fake.calls) != 1 || fake.calls[0].tool != "search_contact_by_key_word" {
t.Fatalf("ambiguous member reached profile/create: %#v", fake.calls)
}
}
func TestCrossPlatformCoverageChatCreateNaturalMemberDryRunUsesSameResolutionChain(t *testing.T) {
fake := &larkAlignmentCaller{responses: map[string]string{
"contact/search_contact_by_key_word": `{"result":[{"name":"张三","userId":"resolved-user"}]}`,
}}
helpers.InitDeps(fake)
root := newPlatformCoverageRoot()
root.SetArgs([]string{
"chat", "+chat-create",
"--name", "测试群",
"--member-query", "张三",
"--dry-run",
"--yes",
})
if err := root.Execute(); err != nil {
t.Fatal(err)
}
if len(fake.calls) != 3 ||
fake.calls[0].tool != "search_contact_by_key_word" ||
fake.calls[1].tool != "get_current_user_profile" ||
fake.calls[2].tool != "create_group_conversation" {
t.Fatalf("dry-run calls = %#v", fake.calls)
}
}
func TestCrossPlatformCoverageMessagesSendRoutesIdentitySpecificTransports(t *testing.T) {
tests := []struct {
name string
args []string
product string
tool string
want map[string]any
bodyKey string
body string
}{
{
name: "user",
args: []string{"chat", "+messages-send", "--as", "user", "--chat-id", "cid", "--markdown", "hello @" + fixtureCurrentDOpenID, "--at-open-dingtalk-ids", fixtureCurrentDOpenID, "--at-all", "--idempotency-key", "u1", "--yes"},
product: "chat",
tool: "send_personal_message",
want: map[string]any{"openConversationId": "cid", "msgType": "markdown", "uuid": "u1"},
bodyKey: "content",
body: "<@all> hello <@" + fixtureCurrentDOpenID + ">",
},
{
name: "bot",
args: []string{"chat", "+messages-send", "--identity", "bot", "--robot-code", "robot", "--group", "cid", "--text", "<@u1> hello", "--at-user-ids", "u1", "--at-all", "--yes"},
product: "bot",
tool: "send_robot_group_message",
want: map[string]any{"robotCode": "robot", "openConversationId": "cid", "isAtAll": "true"},
bodyKey: "markdown",
body: "@all @u1 hello",
},
{
name: "webhook",
args: []string{"chat", "+messages-send", "--identity", "webhook", "--webhook-token", "token", "--text", "<@13800000000> hello", "--at-mobiles", "13800000000", "--at-all", "--yes"},
product: "bot",
tool: "send_message_by_custom_robot",
want: map[string]any{"robotToken": "token", "isAtAll": true},
bodyKey: "text",
body: "@all @13800000000 hello",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fake := &larkAlignmentCaller{}
helpers.InitDeps(fake)
root := newPlatformCoverageRoot()
root.SetArgs(tt.args)
if err := root.Execute(); err != nil {
t.Fatal(err)
}
if len(fake.calls) != 1 {
t.Fatalf("calls = %#v", fake.calls)
}
call := fake.calls[0]
if call.product != tt.product || call.tool != tt.tool {
t.Fatalf("call = %#v", call)
}
for key, want := range tt.want {
if !reflect.DeepEqual(call.args[key], want) {
t.Errorf("%s = %#v, want %#v", key, call.args[key], want)
}
}
if tt.bodyKey == "content" {
rawContent := call.args[tt.bodyKey].(string)
if strings.Contains(rawContent, `\u003c`) || strings.Contains(rawContent, `\u003e`) {
t.Errorf("content = %q; current-user mention tokens must remain literal", rawContent)
}
if !strings.Contains(rawContent, "<@"+fixtureCurrentDOpenID+">") {
t.Errorf("content = %q; missing literal current-user mention token", rawContent)
}
var content map[string]string
if err := json.Unmarshal([]byte(rawContent), &content); err != nil {
t.Fatal(err)
}
if content["text"] != tt.body {
t.Errorf("content text = %q, want %q", content["text"], tt.body)
}
} else if call.args[tt.bodyKey] != tt.body {
t.Errorf("%s = %#v, want %q", tt.bodyKey, call.args[tt.bodyKey], tt.body)
}
})
}
}
func TestCrossPlatformCoverageMessagesSendRejectsUnsupportedIdentityCapability(t *testing.T) {
fake := &larkAlignmentCaller{}
helpers.InitDeps(fake)
root := newPlatformCoverageRoot()
root.SetArgs([]string{
"chat", "+messages-send",
"--identity", "bot",
"--robot-code", "robot",
"--group", "cid",
"--text", "hello",
"--uuid", "unsupported",
"--yes",
})
if err := root.Execute(); err == nil {
t.Fatal("bot uuid unexpectedly accepted")
}
if len(fake.calls) != 0 {
t.Fatalf("invalid capability reached lower service: %#v", fake.calls)
}
}
func TestCrossPlatformCoverageLarkAlignmentWriteMappings(t *testing.T) {
tests := []struct {
name string
args []string
product string
tool string
wantArgs map[string]any
}{
{
name: "chat-update-name-only",
args: []string{"chat", "+chat-update", "--group", "cid-fixture-chat-0001", "--name", "新群名", "--yes"},
product: "chat",
tool: "update_group_name",
wantArgs: map[string]any{
"openconversation_id": "cid-fixture-chat-0001",
"group_name": "新群名",
},
},
{
name: "flag-create",
args: []string{"chat", "+flag-create", "--message-id", "msg", "--conversation-id", "cid", "--yes"},
product: "im",
tool: "add_message_favorite",
wantArgs: map[string]any{
"openMessageId": "msg",
"openConversationId": "cid",
},
},
{
name: "flag-cancel",
args: []string{"chat", "+flag-cancel", "--message-id", "msg", "--conversation-id", "cid", "--yes"},
product: "im",
tool: "remove_message_favorite",
wantArgs: map[string]any{
"openMessageId": "msg",
"openConversationId": "cid",
},
},
{
name: "flag-list",
args: []string{"chat", "+flag-list", "--cursor", "3", "--size", "30"},
product: "im",
tool: "list_message_favorites",
wantArgs: map[string]any{
"cursor": 3,
"size": "30",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fake := &larkAlignmentCaller{}
helpers.InitDeps(fake)
root := newPlatformCoverageRoot()
root.SetArgs(tt.args)
if err := root.Execute(); err != nil {
t.Fatal(err)
}
if len(fake.calls) != 1 {
t.Fatalf("calls = %#v, want 1", fake.calls)
}
call := fake.calls[0]
if call.product != tt.product || call.tool != tt.tool || !reflect.DeepEqual(call.args, tt.wantArgs) {
t.Fatalf("call = %#v, want %s/%s %#v", call, tt.product, tt.tool, tt.wantArgs)
}
})
}
}
func TestCrossPlatformCoverageObservedChatRenameAliasResolvesNameBeforeWrite(t *testing.T) {
fake := &larkAlignmentCaller{responses: map[string]string{
"im/search_groups": `{"result":[{"openConversationId":"cid-project","title":"项目评测群"}],"hasMore":false}`,
}}
helpers.InitDeps(fake)
root := newPlatformCoverageRoot()
root.SetArgs([]string{"chat", "+chat-rename", "--group", "项目评测群", "--name", "项目讨论群", "--yes"})
if err := root.Execute(); err != nil {
t.Fatal(err)
}
if len(fake.calls) != 2 || fake.calls[0].tool != "search_groups" || fake.calls[1].tool != "update_group_name" {
t.Fatalf("calls = %#v", fake.calls)
}
if fake.calls[1].args["openconversation_id"] != "cid-project" || fake.calls[1].args["group_name"] != "项目讨论群" {
t.Fatalf("write args = %#v", fake.calls[1].args)
}
}
func TestCrossPlatformCoverageMessagesReplyPublishesPlainTextBoundary(t *testing.T) {
fake := &larkAlignmentCaller{responses: map[string]string{
"chat/send_personal_message": `{"result":{"openMessageId":"new-msg","openConvThreadId":"thread-1","sendStatus":"accepted"}}`,
}}
helpers.InitDeps(fake)
root := newPlatformCoverageRoot()
var output bytes.Buffer
root.SetOut(&output)
root.SetArgs([]string{
"chat", "+messages-reply",
"--conversation-id", "cid",
"--message-id", "msg",
"--ref-sender", fixtureCurrentDOpenID,
"--text", "收到",
"--idempotency-key", "reply-uuid",
"--yes",
})
if err := root.Execute(); err != nil {
t.Fatal(err)
}
if len(fake.calls) != 1 {
t.Fatalf("calls = %#v, want 1", fake.calls)
}
call := fake.calls[0]
if call.product != "chat" || call.tool != "send_personal_message" {
t.Fatalf("reply call = %#v", call)
}
if call.args["openConversationId"] != "cid" || call.args["msgType"] != "reply" || call.args["uuid"] != "reply-uuid" {
t.Fatalf("reply args = %#v", call.args)
}
var content map[string]string
if err := json.Unmarshal([]byte(call.args["content"].(string)), &content); err != nil {
t.Fatal(err)
}
if content["referenceOpenMessageId"] != "msg" ||
content["srcMsgSendOpenDingTalkId"] != fixtureCurrentDOpenID ||
content["replyMsgType"] != "text" ||
content["content"] != "收到" {
t.Fatalf("reply content = %#v", content)
}
var payload map[string]any
if err := json.Unmarshal(output.Bytes(), &payload); err != nil {
t.Fatal(err)
}
if payload["contractVersion"] != "im.message-reply.v1" ||
payload["messageId"] != "new-msg" ||
payload["conversationId"] != "cid" ||
payload["threadId"] != "thread-1" ||
payload["deliveryStatus"] != "accepted" ||
payload["idempotencyKey"] != "reply-uuid" {
t.Fatalf("reply result context = %#v", payload)
}
referenced, _ := payload["referencedMessage"].(map[string]any)
if referenced["messageId"] != "msg" || referenced["resolutionSource"] != "explicit" {
t.Fatalf("referenced message context = %#v", referenced)
}
}
func TestCrossPlatformCoverageMessagesReplyDryRunStopsBeforeWrite(t *testing.T) {
fake := &larkAlignmentCaller{}
helpers.InitDeps(fake)
root := newPlatformCoverageRoot()
root.SetArgs([]string{
"chat", "+messages-reply",
"--conversation-id", "cid",
"--message-id", "msg",
"--ref-sender", fixtureCurrentDOpenID,
"--text", "收到",
"--dry-run",
"--yes",
})
if err := root.Execute(); err != nil {
t.Fatal(err)
}
if len(fake.calls) != 0 {
t.Fatalf("reply dry-run reached write transport: %#v", fake.calls)
}
}
func TestCrossPlatformCoverageFlagBatchContinuesAndPublishesFailureLedger(t *testing.T) {
fake := &larkAlignmentCaller{failTarget: "m2"}
helpers.InitDeps(fake)
root := newPlatformCoverageRoot()
var output bytes.Buffer
root.SetOut(&output)
root.SetArgs([]string{
"chat", "+flag-create",
"--message-ids", "m1,m2",
"--conversation-id", "cid",
"--yes",
})
if err := root.Execute(); err == nil {
t.Fatal("partial batch failure returned success")
}
if len(fake.calls) != 2 {
t.Fatalf("calls = %#v", fake.calls)
}
var payload map[string]any
if err := json.Unmarshal(output.Bytes(), &payload); err != nil {
t.Fatal(err)
}
if payload["ok"] != false || payload["partial"] != true ||
payload["requestedCount"] != float64(2) ||
payload["succeededCount"] != float64(1) ||
payload["failedCount"] != float64(1) {
t.Fatalf("payload = %#v", payload)
}
}
func TestCrossPlatformCoverageConversationSetTopBatchDryRunPublishesActionsWithoutWrites(t *testing.T) {
fake := &larkAlignmentCaller{}
helpers.InitDeps(fake)
root := newPlatformCoverageRoot()
var output bytes.Buffer
root.SetOut(&output)
root.SetArgs([]string{
"chat", "+conversation-set-top",
"--conversation-ids", "cid-1,cid-2",
"--off",
"--dry-run",
"--yes",
})
if err := root.Execute(); err != nil {
t.Fatal(err)
}
if len(fake.calls) != 0 {
t.Fatalf("dry-run reached lower service: %#v", fake.calls)
}
var payload map[string]any
if err := json.Unmarshal(output.Bytes(), &payload); err != nil {
t.Fatal(err)
}
if payload["dry_run"] != true || payload["executed"] != false ||
payload["preview_kind"] != "plan" ||
payload["actionCount"] != float64(2) ||
payload["failedCount"] != float64(0) {
t.Fatalf("payload = %#v", payload)
}
}
func TestCrossPlatformCoverageMessagesMgetDryRunPublishesMultiResourceDownloadPlan(t *testing.T) {
fake := &larkAlignmentCaller{}
helpers.InitDeps(fake)
root := newPlatformCoverageRoot()
var output bytes.Buffer
root.SetOut(&output)
root.SetArgs([]string{
"chat", "+messages-mget",
"--msg-ids", "msg",
"--download-resources",
"--dry-run",
})
if err := root.Execute(); err != nil {
t.Fatal(err)
}
if len(fake.calls) != 1 ||
fake.calls[0].product != "im" ||
fake.calls[0].tool != "list_messages_by_ids" {
t.Fatalf("calls = %#v", fake.calls)
}
var payload map[string]any
if err := json.Unmarshal(output.Bytes(), &payload); err != nil {
t.Fatal(err)
}
ledger, _ := payload["resourceDownloads"].(map[string]any)
if ledger["dryRun"] != true || ledger["requestedCount"] != float64(1) {
t.Fatalf("ledger = %#v", ledger)
}
}
func TestCrossPlatformCoverageMessagesReplyResolvesUserIDBeforeExecution(t *testing.T) {
fake := &larkAlignmentCaller{}
helpers.InitDeps(fake)
root := newPlatformCoverageRoot()
root.SetArgs([]string{
"chat", "+messages-reply",
"--conversation-id", "cid",
"--ref-msg-id", "msg",
"--ref-sender", "user-id",
"--text", "收到",
"--yes",
})
if err := root.Execute(); err != nil {
t.Fatal(err)
}
if len(fake.calls) != 2 ||
fake.calls[0].product != "contact" ||
fake.calls[0].tool != "search_contact_by_key_word" ||
fake.calls[1].tool != "send_personal_message" {
t.Fatalf("calls = %#v", fake.calls)
}
var content map[string]string
if err := json.Unmarshal([]byte(fake.calls[1].args["content"].(string)), &content); err != nil {
t.Fatal(err)
}
if content["srcMsgSendOpenDingTalkId"] != "D-resolved" {
t.Fatalf("content = %#v", content)
}
}
func TestCrossPlatformCoverageMessagesReplyInfersSenderFromReferencedMessage(t *testing.T) {
fake := &larkAlignmentCaller{}
helpers.InitDeps(fake)
root := newPlatformCoverageRoot()
root.SetArgs([]string{
"chat", "+messages-reply",
"--conversation-id", "cid",
"--ref-msg-id", "msg",
"--text", "收到",
"--yes",
})
if err := root.Execute(); err != nil {
t.Fatal(err)
}
if len(fake.calls) != 2 ||
fake.calls[0].product != "im" ||
fake.calls[0].tool != "list_messages_by_ids" ||
fake.calls[1].tool != "send_personal_message" {
t.Fatalf("calls = %#v", fake.calls)
}
var content map[string]string
if err := json.Unmarshal([]byte(fake.calls[1].args["content"].(string)), &content); err != nil {
t.Fatal(err)
}
if content["srcMsgSendOpenDingTalkId"] != "D-inferred" {
t.Fatalf("content = %#v", content)
}
}
func TestCrossPlatformCoverageFindMessageSenderOpenDingTalkIDIgnoresUnrelatedNestedIdentity(t *testing.T) {
message := map[string]any{
"content": map[string]any{
"mentions": []any{
map[string]any{"openDingTalkId": "D-wrong-recipient"},
},
},
"quotedMessage": map[string]any{
"senderOpenDingTalkId": "D-wrong-quoted-sender",
},
}
if got := findMessageSenderOpenDingTalkID(message); got != "" {
t.Fatalf("sender = %q, want empty", got)
}
message["senderInfo"] = map[string]any{"openDingTalkId": "D-correct"}
if got := findMessageSenderOpenDingTalkID(message); got != "D-correct" {
t.Fatalf("sender = %q, want D-correct", got)
}
}
func TestCrossPlatformCoverageChatListDefaultsToGroupsAndSupportsLarkAliases(t *testing.T) {
fake := &larkAlignmentCaller{
responses: map[string]string{
"im/list_all_conversations": `{
"result":{
"hasMore":true,
"nextCursor":"7",
"list":[
{"openConversationId":"cid-group","conversationName":"项目群","singleChat":false,"ownerUserId":"owner-1"},
{"openConversationId":"cid-direct","title":"张三","singleChat":true},
{"openConversationId":"cid-unknown","title":"未知"}
]
}
}`,
},
}
helpers.InitDeps(fake)
root := newPlatformCoverageRoot()
var output bytes.Buffer
root.SetOut(&output)
root.SetArgs([]string{"chat", "+chat-list", "--exclude-muted", "--page-size", "20"})
if err := root.Execute(); err != nil {
t.Fatal(err)
}
if len(fake.calls) != 1 || fake.calls[0].tool != "list_all_conversations" {
t.Fatalf("calls = %#v", fake.calls)
}
if fake.calls[0].args["limit"] != 20 || fake.calls[0].args["excludeMuted"] != true {
t.Fatalf("args = %#v", fake.calls[0].args)
}
var payload map[string]any
if err := json.Unmarshal(output.Bytes(), &payload); err != nil {
t.Fatal(err)
}
if payload["count"] != float64(1) {
t.Fatalf("default group filter count = %#v", payload)
}
chats := payload["chats"].([]any)
chat := chats[0].(map[string]any)
if chat["openConversationId"] != "cid-group" || chat["conversationType"] != "group" || chat["chatMode"] != "group" {
t.Fatalf("chat = %#v", chat)
}
if !reflect.DeepEqual(payload["requestedTypes"], []any{"group"}) {
t.Fatalf("requestedTypes = %#v", payload["requestedTypes"])
}
if filter, _ := payload["filter"].(map[string]any); filter["excludeMuted"] != true {
t.Fatalf("filter = %#v", payload["filter"])
}
}
func TestCrossPlatformCoverageChatListIncludesP2PAndRejectsInvalidTypes(t *testing.T) {
fake := &larkAlignmentCaller{
responses: map[string]string{
"im/list_all_conversations": `{
"result":{"list":[
{"openConversationId":"cid-group","name":"项目群","conversationType":"group"},
{"openConversationId":"cid-direct","name":"李四","conversationType":"P2P"}
]}
}`,
},
}
helpers.InitDeps(fake)
root := newPlatformCoverageRoot()
var output bytes.Buffer
root.SetOut(&output)
root.SetArgs([]string{"chat", "+chat-list", "--types", "group,p2p", "--page-token", "3"})
if err := root.Execute(); err != nil {
t.Fatal(err)
}
if fake.calls[0].args["cursor"] != 3 || fake.calls[0].args["limit"] != 20 {
t.Fatalf("args = %#v", fake.calls[0].args)
}
var payload map[string]any
if err := json.Unmarshal(output.Bytes(), &payload); err != nil {
t.Fatal(err)
}
if payload["count"] != float64(2) {
t.Fatalf("both types count = %#v", payload)
}
chats := payload["chats"].([]any)
direct := chats[1].(map[string]any)
if direct["conversationType"] != "direct" || direct["chatMode"] != "p2p" {
t.Fatalf("direct chat = %#v", direct)
}
helpers.InitDeps(&larkAlignmentCaller{})
root = newPlatformCoverageRoot()
root.SetArgs([]string{"chat", "+chat-list", "--types", "channel"})
if err := root.Execute(); err == nil {
t.Fatal("invalid --types unexpectedly accepted")
}
}
func TestCrossPlatformCoverageChatListP2POnlyDropsGroups(t *testing.T) {
fake := &larkAlignmentCaller{
responses: map[string]string{
"im/list_all_conversations": `{
"result":{"list":[
{"openConversationId":"cid-group","name":"项目群","singleChat":false},
{"openConversationId":"cid-direct","name":"王五","singleChat":true}
]}
}`,
},
}
helpers.InitDeps(fake)
root := newPlatformCoverageRoot()
var output bytes.Buffer
root.SetOut(&output)
root.SetArgs([]string{"chat", "+chat-list", "--types", "p2p", "--limit", "5"})
if err := root.Execute(); err != nil {
t.Fatal(err)
}
if fake.calls[0].args["limit"] != 5 {
t.Fatalf("limit alias args = %#v", fake.calls[0].args)
}
var payload map[string]any
if err := json.Unmarshal(output.Bytes(), &payload); err != nil {
t.Fatal(err)
}
if payload["count"] != float64(1) {
t.Fatalf("p2p-only count = %#v", payload)
}
chat := payload["chats"].([]any)[0].(map[string]any)
if chat["openConversationId"] != "cid-direct" {
t.Fatalf("chat = %#v", chat)
}
}
func TestCrossPlatformCoverageFeedGroupQueryProjectPreservesRequestOrderAndMissingLedger(t *testing.T) {
conversations := []map[string]any{
{"openConversationId": "cid-a", "conversationName": "A"},
{"openConversationId": "cid-b", "conversationName": "B"},
}
got := feedGroupQueryProject(conversations, []string{"cid-b", "cid-missing", "cid-a", "cid-b"})
if got["requestedCount"] != 3 || got["foundCount"] != 2 {
t.Fatalf("counts = %#v", got)
}
items := got["items"].([]map[string]any)
if items[0]["openConversationId"] != "cid-b" || items[1]["openConversationId"] != "cid-a" {
t.Fatalf("items = %#v", items)
}
if missing := got["notFoundConversationIds"]; !reflect.DeepEqual(missing, []string{"cid-missing"}) {
t.Fatalf("missing = %#v", missing)
}
}
func TestCrossPlatformCoverageFeedGroupQueryDoesNotMisreportMissingItemWhenSourceHasMore(t *testing.T) {
fake := &larkAlignmentCaller{
category: `{"result":{"hasMore":true,"list":[{"openConversationId":"cid-a","conversationName":"A"}]}}`,
}
helpers.InitDeps(fake)
root := newPlatformCoverageRoot()
var output bytes.Buffer
root.SetOut(&output)
root.SetArgs([]string{
"chat", "+feed-group-query-item",
"--category-id", "1",
"--conversation-ids", "cid-a,cid-later",
})
if err := root.Execute(); err != nil {
t.Fatal(err)
}
var payload map[string]any
if err := json.Unmarshal(output.Bytes(), &payload); err != nil {
t.Fatal(err)
}
if payload["complete"] != false ||
payload["notFoundCount"] != float64(0) ||
payload["unresolvedCount"] != float64(1) ||
payload["failedCount"] != float64(1) {
t.Fatalf("payload = %#v", payload)
}
unresolved, _ := payload["unresolvedConversationIds"].([]any)
if len(unresolved) != 1 || unresolved[0] != "cid-later" {
t.Fatalf("unresolved = %#v", unresolved)
}
}