Files
dingtalk-workspace-cli/internal/shortcut/chat/unified_send_resolution_test.go
T

178 lines
6.0 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 (
stderrors "errors"
"testing"
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/helpers"
)
func TestCrossPlatformCoverageMessagesSendResolvesNaturalUserAndChatTargets(t *testing.T) {
tests := []struct {
name string
args []string
responses map[string]string
wantTarget string
wantValue string
}{
{
name: "user query",
args: []string{
"chat", "+messages-send", "--as", "user",
"--user-query", "张三", "--text", "你好", "--yes",
},
responses: map[string]string{
"contact/search_contact_by_key_word": `{"result":[{"name":"张三","userId":"u1","openDingTalkId":"D1"}]}`,
},
wantTarget: "receiverOpenDingTalkId",
wantValue: "D1",
},
{
name: "chat query exact wins",
args: []string{
"chat", "+messages-send", "--as", "user",
"--chat-query", "项目群", "--text", "你好", "--yes",
},
responses: map[string]string{
"im/search_groups": `{"result":[{"title":"项目群-归档","openConversationId":"c2"},{"title":"项目群","openConversationId":"c1"}]}`,
},
wantTarget: "openConversationId",
wantValue: "c1",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fake := &larkAlignmentCaller{responses: tt.responses}
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 + send", fake.calls)
}
send := fake.calls[1]
if send.product != "chat" || send.tool != "send_personal_message" {
t.Fatalf("send = %#v", send)
}
if send.args[tt.wantTarget] != tt.wantValue {
t.Fatalf("%s = %#v, want %q", tt.wantTarget, send.args[tt.wantTarget], tt.wantValue)
}
})
}
}
func TestCrossPlatformCoverageMessagesSendNaturalTargetAmbiguityHasNoWrite(t *testing.T) {
fake := &larkAlignmentCaller{responses: map[string]string{
"contact/search_contact_by_key_word": `{"result":[{"name":"张三","userId":"u1","openDingTalkId":"D1"},{"name":"张三","userId":"u2","openDingTalkId":"D2"}]}`,
}}
helpers.InitDeps(fake)
root := newPlatformCoverageRoot()
root.SetArgs([]string{
"chat", "+messages-send", "--as", "user",
"--user-query", "张三", "--text", "你好", "--yes",
})
err := root.Execute()
if err == nil {
t.Fatal("ambiguous user unexpectedly sent")
}
if len(fake.calls) != 1 || fake.calls[0].tool != "search_contact_by_key_word" {
t.Fatalf("ambiguous resolution reached write: %#v", fake.calls)
}
var typed *apperrors.Error
if !stderrors.As(err, &typed) {
t.Fatalf("error type = %T", err)
}
if typed.Reason != "resolution_ambiguous" || typed.Details["type"] != "resolution" {
t.Fatalf("structured error = %#v", typed)
}
}
func TestCrossPlatformCoverageMessagesSendChatQueryResolvesAllPagesBeforeWrite(t *testing.T) {
fake := &larkAlignmentCaller{sequenceResponses: map[string][]string{
"im/search_groups": {
`{"result":[{"title":"项目群-归档","openConversationId":"archive"}],"hasMore":true,"nextCursor":"page-2"}`,
`{"result":[{"title":"项目群","openConversationId":"active"}],"hasMore":false}`,
},
}}
helpers.InitDeps(fake)
root := newPlatformCoverageRoot()
root.SetArgs([]string{
"chat", "+messages-send", "--as", "user",
"--chat-query", "项目群", "--text", "你好", "--yes",
})
if err := root.Execute(); err != nil {
t.Fatal(err)
}
if len(fake.calls) != 3 || fake.calls[0].tool != "search_groups" ||
fake.calls[1].tool != "search_groups" || fake.calls[2].tool != "send_personal_message" {
t.Fatalf("calls = %#v, want two resolution pages then one write", fake.calls)
}
if fake.calls[1].args["cursor"] != "page-2" || fake.calls[2].args["openConversationId"] != "active" {
t.Fatalf("calls = %#v", fake.calls)
}
}
func TestCrossPlatformCoverageMessagesSendIncompleteChatResolutionHasNoWrite(t *testing.T) {
fake := &larkAlignmentCaller{responses: map[string]string{
"im/search_groups": `{"result":[{"title":"项目群","openConversationId":"c1"}],"hasMore":true}`,
}}
helpers.InitDeps(fake)
root := newPlatformCoverageRoot()
root.SetArgs([]string{
"chat", "+messages-send", "--as", "user",
"--chat-query", "项目群", "--text", "你好", "--yes",
})
err := root.Execute()
var typed *apperrors.Error
if !stderrors.As(err, &typed) || typed.Reason != "resolution_incomplete" {
t.Fatalf("error = %#v", err)
}
if len(fake.calls) != 1 || fake.calls[0].tool != "search_groups" {
t.Fatalf("incomplete resolution reached write: %#v", fake.calls)
}
}
func TestCrossPlatformCoverageMessagesSendDryRunUsesRealNaturalTargetResolution(t *testing.T) {
fake := &larkAlignmentCaller{responses: map[string]string{
"im/search_groups": `{"result":[{"title":"项目群","openConversationId":"c1"}]}`,
}}
helpers.InitDeps(fake)
root := newPlatformCoverageRoot()
root.SetArgs([]string{
"chat", "+messages-send", "--as", "user",
"--chat-query", "项目群", "--text", "你好", "--dry-run", "--yes",
})
if err := root.Execute(); err != nil {
t.Fatal(err)
}
if len(fake.calls) != 1 {
t.Fatalf("dry-run calls = %#v, want one read-only resolution", fake.calls)
}
if fake.calls[0].product != "im" || fake.calls[0].tool != "search_groups" {
t.Fatalf("dry-run resolution = %#v", fake.calls[0])
}
}
func TestCrossPlatformCoverageMessagesSendRejectsNaturalTargetForUnsupportedIdentity(t *testing.T) {
fake := &larkAlignmentCaller{}
helpers.InitDeps(fake)
root := newPlatformCoverageRoot()
root.SetArgs([]string{
"chat", "+messages-send", "--as", "bot", "--robot-code", "r",
"--chat-query", "项目群", "--text", "你好", "--yes",
})
if err := root.Execute(); err == nil {
t.Fatal("bot natural target unexpectedly accepted")
}
if len(fake.calls) != 0 {
t.Fatalf("invalid identity reached lower service: %#v", fake.calls)
}
}