Files
dingtalk-workspace-cli/internal/shortcut/smart/natural_target_resolution_test.go
T

510 lines
18 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 smart
import (
"bytes"
"encoding/json"
"reflect"
"strings"
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/helpers"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut"
)
func TestCrossPlatformCoverageNaturalSearchAndSenderFlagsStayPublicOptional(t *testing.T) {
tests := []struct {
name string
flags []shortcut.Flag
want []string
}{
{name: "search natural adapters", flags: SearchMsg.Flags, want: []string{"chat-query", "sender-query"}},
{name: "chat messages natural adapters", flags: ChatMessages.Flags, want: []string{"chat-query", "sender", "sender-query"}},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
for _, name := range tc.want {
found := false
for _, flag := range tc.flags {
if flag.Name != name {
continue
}
found = true
if flag.Hidden {
t.Fatalf("--%s unexpectedly became hidden", name)
}
if flag.Required {
t.Fatalf("--%s unexpectedly became required", name)
}
break
}
if !found {
t.Fatalf("public optional flag --%s is missing", name)
}
}
})
}
}
func TestCrossPlatformCoverageMessageTimeAlignmentFlagsStayPublicOptional(t *testing.T) {
for _, tc := range []struct {
name string
flags []shortcut.Flag
}{
{name: "search message", flags: SearchMsg.Flags},
{name: "conversation message", flags: ChatMessages.Flags},
} {
t.Run(tc.name, func(t *testing.T) {
for _, name := range []string{"start", "start-time", "end", "end-time", "order", "sort"} {
found := false
for _, flag := range tc.flags {
if flag.Name != name {
continue
}
found = true
if flag.Hidden || flag.Required {
t.Fatalf("--%s must stay public and optional: %#v", name, flag)
}
if (name == "order" || name == "sort") && !reflect.DeepEqual(flag.Enum, []string{"asc", "desc"}) {
t.Fatalf("--%s enum = %#v", name, flag.Enum)
}
break
}
if !found {
t.Fatalf("public optional flag --%s is missing", name)
}
}
})
}
}
func TestCrossPlatformCoverageChatMessagesResolvesNaturalChatAndUserTargets(t *testing.T) {
tests := []struct {
name string
args []string
wantTool string
wantKey string
wantValue string
}{
{
name: "chat query",
args: []string{"chat", "+chat-messages", "--chat-query", "项目冲刺"},
wantTool: "list_conversation_message_v2",
wantKey: "openconversation_id",
wantValue: "cid-1",
},
{
name: "natural group through group flag",
args: []string{"chat", "+chat-messages", "--group", "项目冲刺"},
wantTool: "list_conversation_message_v2",
wantKey: "openconversation_id",
wantValue: "cid-1",
},
{
name: "user query",
args: []string{"chat", "+chat-messages", "--user-query", "张三"},
wantTool: "list_individual_chat_message",
wantKey: "openDingTalkId",
wantValue: "open1",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fake := &platformCoverageCaller{}
helpers.InitDeps(fake)
root := newPlatformCoverageRoot()
root.SetArgs(tt.args)
if err := root.Execute(); err != nil {
t.Fatal(err)
}
if len(fake.calls) != 2 {
t.Fatalf("calls = %#v, want resolve + read", fake.calls)
}
read := fake.calls[1]
if read.tool != tt.wantTool || read.args[tt.wantKey] != tt.wantValue {
t.Fatalf("read = %#v", read)
}
})
}
}
func TestCrossPlatformCoverageChatMessagesStableGroupBypassesNaturalResolution(t *testing.T) {
fake := &platformCoverageCaller{}
helpers.InitDeps(fake)
root := newPlatformCoverageRoot()
root.SetArgs([]string{"chat", "+chat-messages", "--group", "cid-fixture-chat-0001"})
if err := root.Execute(); err != nil {
t.Fatal(err)
}
if len(fake.calls) != 1 || fake.calls[0].tool != "list_conversation_message_v2" ||
fake.calls[0].args["openconversation_id"] != "cid-fixture-chat-0001" {
t.Fatalf("calls = %#v", fake.calls)
}
}
func TestCrossPlatformCoverageChatMessagesOptionallyFiltersResolvedSenderByEitherStableID(t *testing.T) {
fake := &platformCoverageCaller{
contactSearchResult: `{"result":[{"name":"测试用户甲","userId":"uid-fixture-sender-1","openDingTalkId":"D1"}]}`,
chatMessagesResult: `{"result":{"hasMore":false,"messages":[
{"openMessageId":"m-open","sender":"群昵称甲","senderOpenDingTalkId":"D1","content":"open id match"},
{"openMessageId":"m-user","sender":"群昵称甲","senderUserId":"uid-fixture-sender-1","content":"user id match"},
{"openMessageId":"m-other","sender":"其他人","senderOpenDingTalkId":"D2","content":"other"}
]}}`,
}
helpers.InitDeps(fake)
root := newPlatformCoverageRoot()
var output bytes.Buffer
root.SetOut(&output)
root.SetArgs([]string{
"chat", "+chat-messages", "--group", "cid-fixture-chat-0001",
"--sender", "测试用户甲", "--page-all",
})
if err := root.Execute(); err != nil {
t.Fatal(err)
}
if len(fake.calls) != 2 || fake.calls[0].tool != "list_conversation_message_v2" ||
fake.calls[1].tool != "search_contact_by_key_word" {
t.Fatalf("calls = %#v, want message read followed by optional sender resolve", fake.calls)
}
var payload map[string]any
if err := json.Unmarshal(output.Bytes(), &payload); err != nil {
t.Fatalf("decode output: %v\n%s", err, output.String())
}
if payload["complete"] != true || payload["count"] != float64(2) {
t.Fatalf("filtered payload = %#v", payload)
}
filters, ok := payload["resolvedFilters"].(map[string]any)
if !ok {
t.Fatalf("resolvedFilters missing: %#v", payload)
}
senders, ok := filters["senders"].([]any)
if !ok || len(senders) != 1 {
t.Fatalf("resolved senders = %#v", filters["senders"])
}
selected := senders[0].(map[string]any)["selected"].(map[string]any)
if selected["userId"] != "uid-fixture-sender-1" || selected["openDingTalkId"] != "D1" {
t.Fatalf("selected identity = %#v", selected)
}
messages, ok := payload["messages"].([]any)
if !ok || len(messages) != 2 {
t.Fatalf("messages = %#v", payload["messages"])
}
for _, raw := range messages {
message := raw.(map[string]any)
if message["sender"] != "群昵称甲" || message["senderId"] == "D2" {
t.Fatalf("unexpected filtered message = %#v", message)
}
}
}
func TestChatMessagesFormatValidSenderSkipsDirectoryAndFiltersStableID(t *testing.T) {
fake := &platformCoverageCaller{
chatMessagesResult: `{"result":{"hasMore":false,"messages":[
{"openMessageId":"wanted","sender":"目标展示名","senderOpenDingTalkId":"DAAAAAAAAAAAiE","content":"保留"},
{"openMessageId":"other","sender":"其他人","senderOpenDingTalkId":"DAQEBAQEBAQEiE","content":"过滤"}
]}}`,
}
helpers.InitDeps(fake)
root := newPlatformCoverageRoot()
var output bytes.Buffer
root.SetOut(&output)
root.SetArgs([]string{
"chat", "+chat-messages", "--group", "cid-fixture-chat-0001",
"--sender", "DAAAAAAAAAAAiE", "--page-all",
})
if err := root.Execute(); err != nil {
t.Fatal(err)
}
if len(fake.calls) != 1 || fake.calls[0].tool != "list_conversation_message_v2" {
t.Fatalf("format-valid sender unexpectedly preflighted: %#v", fake.calls)
}
var payload map[string]any
if err := json.Unmarshal(output.Bytes(), &payload); err != nil {
t.Fatal(err)
}
if payload["count"] != float64(1) || payload["complete"] != true {
t.Fatalf("payload=%#v", payload)
}
}
func TestChatMessagesWithoutSenderDoesNotResolveEveryMessageIdentity(t *testing.T) {
fake := &platformCoverageCaller{chatMessagesResult: `{"result":{"hasMore":false,"messages":[
{"openMessageId":"m1","sender":"群昵称一","senderOpenDingTalkId":"D1","content":"一"},
{"openMessageId":"m2","sender":"群昵称二","senderOpenDingTalkId":"D2","content":"二"}
]}}`}
helpers.InitDeps(fake)
root := newPlatformCoverageRoot()
var output bytes.Buffer
root.SetOut(&output)
root.SetArgs([]string{"chat", "+chat-messages", "--group", "cid-fixture-chat-0001", "--page-all"})
if err := root.Execute(); err != nil {
t.Fatal(err)
}
if len(fake.calls) != 1 || fake.calls[0].tool != "list_conversation_message_v2" {
t.Fatalf("calls=%#v", fake.calls)
}
var payload map[string]any
if err := json.Unmarshal(output.Bytes(), &payload); err != nil {
t.Fatal(err)
}
identity := payload["identityResult"].(map[string]any)
if payload["count"] != float64(2) || identity["status"] != "requires_query_check" ||
identity["negativeConclusionAllowed"] != false {
t.Fatalf("payload=%#v", payload)
}
}
func TestCrossPlatformCoverageChatMessagesSenderResolutionFailureStopsWithoutUnfilteredMessages(t *testing.T) {
fake := &platformCoverageCaller{
contactSearchResult: `{"result":[]}`,
chatMessagesResult: `{"result":{"hasMore":false,"messages":[
{"openMessageId":"m1","sender":"群昵称甲","senderOpenDingTalkId":"D1","content":"仍然返回"}
]}}`,
}
helpers.InitDeps(fake)
root := newPlatformCoverageRoot()
var output bytes.Buffer
root.SetOut(&output)
root.SetArgs([]string{
"chat", "+chat-messages", "--group", "cid-fixture-chat-0001",
"--sender-query", "不存在的人", "--page-all",
})
if err := root.Execute(); err == nil {
t.Fatal("sender resolution failure unexpectedly succeeded")
}
if len(fake.calls) != 2 || fake.calls[0].tool != "list_conversation_message_v2" ||
fake.calls[1].tool != "search_contact_by_key_word" {
t.Fatalf("calls = %#v, want message read followed by failed resolve", fake.calls)
}
var payload map[string]any
if err := json.Unmarshal(output.Bytes(), &payload); err != nil {
t.Fatalf("decode output: %v\n%s", err, output.String())
}
if payload["count"] != float64(0) || payload["complete"] != false ||
payload["partial"] != false || payload["failedCount"] != float64(1) ||
payload["stopReason"] != "sender_resolution_failed" {
t.Fatalf("fail-closed payload = %#v", payload)
}
messages, ok := payload["messages"].([]any)
if !ok || len(messages) != 0 {
t.Fatalf("failed resolution leaked unfiltered messages: %#v", payload["messages"])
}
if _, exists := payload["resolvedFilters"]; exists {
t.Fatalf("failed resolution unexpectedly published resolvedFilters: %#v", payload)
}
failures, ok := payload["failures"].([]any)
if !ok || len(failures) != 1 || failures[0].(map[string]any)["stage"] != "sender_resolution" {
t.Fatalf("resolution failures = %#v", payload["failures"])
}
}
func TestCrossPlatformCoverageChatMessagesNaturalUserAmbiguityStopsBeforeMessageRead(t *testing.T) {
fake := &platformCoverageCaller{contactSearchResult: `{"result":[{"name":"张三","userId":"u1","openDingTalkId":"D1"},{"name":"张三","userId":"u2","openDingTalkId":"D2"}]}`}
helpers.InitDeps(fake)
root := newPlatformCoverageRoot()
root.SetArgs([]string{"chat", "+chat-messages", "--user-query", "张三"})
if err := root.Execute(); err == nil {
t.Fatal("ambiguous user unexpectedly reached message read")
}
if len(fake.calls) != 1 || fake.calls[0].tool != "search_contact_by_key_word" {
t.Fatalf("calls = %#v", fake.calls)
}
}
func TestCrossPlatformCoverageChatMessagesRejectsConversationIDInPeerIdentityFlag(t *testing.T) {
fake := &platformCoverageCaller{}
helpers.InitDeps(fake)
root := newPlatformCoverageRoot()
root.SetArgs([]string{"chat", "+chat-messages", "--open-dingtalk-id", "cid-fixture-chat-0001"})
err := root.Execute()
if err == nil || !strings.Contains(err.Error(), "--group") {
t.Fatalf("error = %v", err)
}
if len(fake.calls) != 0 {
t.Fatalf("invalid identity reached lower API: %#v", fake.calls)
}
}
func TestCrossPlatformCoverageAtMeResolvesNaturalGroupBeforeSearch(t *testing.T) {
fake := &platformCoverageCaller{}
helpers.InitDeps(fake)
root := newPlatformCoverageRoot()
root.SetArgs([]string{"chat", "+at-me", "--chat-query", "项目冲刺"})
if err := root.Execute(); err != nil {
t.Fatal(err)
}
if len(fake.calls) != 2 || fake.calls[1].tool != "search_at_me_message" || fake.calls[1].args["openConversationId"] != "cid-1" {
t.Fatalf("calls = %#v", fake.calls)
}
}
func TestCrossPlatformCoverageAtMeStableIDInQueryBypassesNaturalResolution(t *testing.T) {
fake := &platformCoverageCaller{}
helpers.InitDeps(fake)
root := newPlatformCoverageRoot()
root.SetArgs([]string{"chat", "+at-me", "--chat-query", "cid-fixture-chat-0001"})
if err := root.Execute(); err != nil {
t.Fatal(err)
}
if len(fake.calls) != 1 || fake.calls[0].tool != "search_at_me_message" ||
fake.calls[0].args["openConversationId"] != "cid-fixture-chat-0001" {
t.Fatalf("calls = %#v", fake.calls)
}
}
func TestCrossPlatformCoverageSendToGroupStableIDBypassesNaturalResolution(t *testing.T) {
fake := &platformCoverageCaller{}
helpers.InitDeps(fake)
root := newPlatformCoverageRoot()
root.SetArgs([]string{"chat", "+send-to-group", "--group", "cid-fixture-chat-0001", "--text", "评测", "--yes"})
if err := root.Execute(); err != nil {
t.Fatal(err)
}
if len(fake.calls) != 1 || fake.calls[0].tool != "send_personal_message" ||
fake.calls[0].args["openConversationId"] != "cid-fixture-chat-0001" {
t.Fatalf("calls = %#v", fake.calls)
}
}
func TestCrossPlatformCoverageSearchMsgResolvesNaturalChatAndSenderBeforeSearch(t *testing.T) {
fake := &platformCoverageCaller{contactSearchResult: `{"result":[{"name":"张三","userId":"u1","openDingTalkId":"D1"}]}`}
helpers.InitDeps(fake)
root := newPlatformCoverageRoot()
root.SetArgs([]string{
"chat", "+search-msg",
"--chat-query", "项目冲刺",
"--sender-query", "张三",
"--no-enrich",
})
if err := root.Execute(); err != nil {
t.Fatal(err)
}
if len(fake.calls) != 4 {
t.Fatalf("calls = %#v, want chat resolve + user resolve + scope validation + search", fake.calls)
}
if preflight := fake.calls[2]; preflight.product != "chat" || preflight.tool != "get_conversation_info" || preflight.args["openConversationId"] != "cid-1" {
t.Fatalf("scope preflight = %#v", preflight)
}
search := fake.calls[3]
if search.product != "im" || search.tool != "search_messages" {
t.Fatalf("search = %#v", search)
}
if _, exists := search.args["openConversationIds"]; exists {
t.Fatalf("global fallback unexpectedly forwarded openConversationIds: %#v", search.args)
}
if got, want := search.args["senderOpenDingTakIds"], []string{"D1"}; !reflect.DeepEqual(got, want) {
t.Fatalf("senderOpenDingTakIds = %#v, want %#v", got, want)
}
}
func TestCrossPlatformCoverageSearchMsgPreservesResolvedSenderWhenDisplayNameDiffers(t *testing.T) {
fake := &platformCoverageCaller{
contactSearchResult: `{"result":[{"name":"测试用户甲","userId":"uid-fixture-sender-1","openDingTalkId":"D1"}]}`,
searchMessagesResult: `{"result":{"messages":[{"openMessageId":"m1","openConversationId":"cid-1","sender":"群昵称甲","senderOpenDingTalkId":"D1","content":"评测消息"}],"hasMore":false}}`,
}
helpers.InitDeps(fake)
root := newPlatformCoverageRoot()
var output bytes.Buffer
root.SetOut(&output)
root.SetArgs([]string{"chat", "+search-msg", "--sender-query", "测试用户甲", "--no-enrich"})
if err := root.Execute(); err != nil {
t.Fatal(err)
}
var payload map[string]any
if err := json.Unmarshal(output.Bytes(), &payload); err != nil {
t.Fatalf("decode output: %v\n%s", err, output.String())
}
filters, ok := payload["resolvedFilters"].(map[string]any)
if !ok {
t.Fatalf("resolvedFilters missing: %#v", payload)
}
senders, ok := filters["senders"].([]any)
if !ok || len(senders) != 1 {
t.Fatalf("resolved senders = %#v", filters["senders"])
}
resolution := senders[0].(map[string]any)
selected := resolution["selected"].(map[string]any)
if resolution["query"] != "测试用户甲" || selected["openDingTalkId"] != "D1" {
t.Fatalf("sender resolution = %#v", resolution)
}
messages, ok := payload["messages"].([]any)
if !ok || len(messages) != 1 {
t.Fatalf("messages = %#v", payload["messages"])
}
message := messages[0].(map[string]any)
if message["sender"] != "群昵称甲" || message["senderId"] != "D1" {
t.Fatalf("projected message = %#v", message)
}
}
func TestCrossPlatformCoverageSearchMsgAcceptsStableIDInChatQuery(t *testing.T) {
fake := &platformCoverageCaller{}
helpers.InitDeps(fake)
root := newPlatformCoverageRoot()
root.SetArgs([]string{
"chat", "+search-msg",
"--chat-query", "cid-fixture-chat-0002",
"--text", "评测",
"--no-enrich",
})
if err := root.Execute(); err != nil {
t.Fatal(err)
}
if len(fake.calls) != 2 || fake.calls[0].tool != "get_conversation_info" || fake.calls[1].tool != "search_messages" {
t.Fatalf("calls = %#v", fake.calls)
}
if fake.calls[0].args["openConversationId"] != "cid-fixture-chat-0002" {
t.Fatalf("scope preflight = %#v", fake.calls[0])
}
if _, exists := fake.calls[1].args["openConversationIds"]; exists {
t.Fatalf("global fallback unexpectedly forwarded openConversationIds: %#v", fake.calls[1].args)
}
if fake.calls[1].args["keyword"] != "评测" {
t.Fatalf("keyword = %#v", fake.calls[1].args["keyword"])
}
}
func TestCrossPlatformCoverageChatMembersListGroupAcceptsNameAndStableID(t *testing.T) {
for _, tt := range []struct {
name string
args []string
wantCalls int
}{
{name: "name", args: []string{"chat", "+chat-members-list", "--group", "项目冲刺", "--member-types", "user"}, wantCalls: 2},
{name: "stable id", args: []string{"chat", "+chat-members-list", "--group", "cid-fixture-chat-0001", "--member-types", "user"}, wantCalls: 1},
{name: "compat alias", args: []string{"chat", "+chat-members-list", "--open-conversation-id", "cid-short-placeholder", "--member-types", "user"}, wantCalls: 1},
} {
t.Run(tt.name, func(t *testing.T) {
fake := &platformCoverageCaller{}
helpers.InitDeps(fake)
root := newPlatformCoverageRoot()
root.SetArgs(tt.args)
if err := root.Execute(); err != nil {
t.Fatal(err)
}
if len(fake.calls) != tt.wantCalls || fake.calls[len(fake.calls)-1].tool != "get_group_members" {
t.Fatalf("calls = %#v", fake.calls)
}
})
}
}
func TestCrossPlatformCoverageSearchMsgNaturalSenderAmbiguityStopsBeforeSearch(t *testing.T) {
fake := &platformCoverageCaller{contactSearchResult: `{"result":[{"name":"张三","userId":"u1","openDingTalkId":"D1"},{"name":"张三","userId":"u2","openDingTalkId":"D2"}]}`}
helpers.InitDeps(fake)
root := newPlatformCoverageRoot()
root.SetArgs([]string{"chat", "+search-msg", "--sender-query", "张三", "--no-enrich"})
if err := root.Execute(); err == nil {
t.Fatal("ambiguous sender unexpectedly reached search")
}
if len(fake.calls) != 1 || fake.calls[0].tool != "search_contact_by_key_word" {
t.Fatalf("calls = %#v", fake.calls)
}
}