229 lines
7.9 KiB
Go
229 lines
7.9 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.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package helpers
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"os"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition"
|
|
)
|
|
|
|
type chatMessageSearchCall struct {
|
|
productID string
|
|
toolName string
|
|
args map[string]any
|
|
}
|
|
|
|
type chatMessageSearchCaller struct {
|
|
calls []chatMessageSearchCall
|
|
}
|
|
|
|
func (c *chatMessageSearchCaller) CallTool(_ context.Context, productID, toolName string, args map[string]any) (*edition.ToolResult, error) {
|
|
c.calls = append(c.calls, chatMessageSearchCall{productID: productID, toolName: toolName, args: args})
|
|
return &edition.ToolResult{Content: []edition.ContentBlock{{Type: "text", Text: `{}`}}}, nil
|
|
}
|
|
|
|
func (*chatMessageSearchCaller) Format() string { return "json" }
|
|
func (*chatMessageSearchCaller) DryRun() bool { return false }
|
|
func (*chatMessageSearchCaller) Fields() string { return "" }
|
|
func (*chatMessageSearchCaller) JQ() string { return "" }
|
|
|
|
func TestCrossPlatformCoverageChatMessageSearchUsesMCPContracts(t *testing.T) {
|
|
previousDeps := deps
|
|
previousArgs := os.Args
|
|
t.Cleanup(func() {
|
|
deps = previousDeps
|
|
os.Args = previousArgs
|
|
})
|
|
|
|
start := "2026-07-09T00:00:00+08:00"
|
|
end := "2026-07-11T00:00:00+08:00"
|
|
startTime, err := time.Parse(time.RFC3339, start)
|
|
if err != nil {
|
|
t.Fatalf("parse start time: %v", err)
|
|
}
|
|
endTime, err := time.Parse(time.RFC3339, end)
|
|
if err != nil {
|
|
t.Fatalf("parse end time: %v", err)
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
args []string
|
|
productID string
|
|
toolName string
|
|
wantToolArg map[string]any
|
|
}{
|
|
{
|
|
name: "keyword search",
|
|
args: []string{"message", "search", "--query", "categoryName", "--group", "cid-1", "--start", start, "--end", end, "--limit", "100", "--cursor", "0"},
|
|
productID: "chat",
|
|
toolName: "search_messages_by_keyword",
|
|
wantToolArg: map[string]any{
|
|
"keyword": "categoryName",
|
|
"openConversationId": "cid-1",
|
|
"startTime": startTime.UnixMilli(),
|
|
"endTime": endTime.UnixMilli(),
|
|
"limit": 100,
|
|
"cursor": "0",
|
|
},
|
|
},
|
|
{
|
|
name: "advanced search",
|
|
args: []string{"message", "search-advanced", "--query", "categoryName", "--conversation-ids", "cid-1,cid-2", "--start", start, "--end", end, "--limit", "100", "--cursor", "0"},
|
|
productID: "im",
|
|
toolName: "search_messages",
|
|
wantToolArg: map[string]any{
|
|
"keyword": "categoryName",
|
|
"openConversationIds": []string{"cid-1", "cid-2"},
|
|
"startTime": startTime.UnixMilli(),
|
|
"endTime": endTime.UnixMilli(),
|
|
"limit": 100,
|
|
"cursor": "0",
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
caller := &chatMessageSearchCaller{}
|
|
InitDeps(caller)
|
|
deps.Out.w = io.Discard
|
|
os.Args = []string{"dws", "chat"}
|
|
|
|
cmd := newChatCommand()
|
|
cmd.SilenceErrors = true
|
|
cmd.SilenceUsage = true
|
|
cmd.SetArgs(tt.args)
|
|
if err := cmd.Execute(); err != nil {
|
|
t.Fatalf("chat search returned error: %v", err)
|
|
}
|
|
if len(caller.calls) != 1 {
|
|
t.Fatalf("tool call count = %d, want 1", len(caller.calls))
|
|
}
|
|
call := caller.calls[0]
|
|
if call.productID != tt.productID || call.toolName != tt.toolName {
|
|
t.Fatalf("tool call = %s/%s, want %s/%s", call.productID, call.toolName, tt.productID, tt.toolName)
|
|
}
|
|
if !reflect.DeepEqual(call.args, tt.wantToolArg) {
|
|
t.Fatalf("tool args = %#v, want %#v", call.args, tt.wantToolArg)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
type chatChangedContractCaller struct {
|
|
calls []chatMessageSearchCall
|
|
resolveUsers bool
|
|
}
|
|
|
|
func (c *chatChangedContractCaller) CallTool(_ context.Context, productID, toolName string, args map[string]any) (*edition.ToolResult, error) {
|
|
c.calls = append(c.calls, chatMessageSearchCall{productID: productID, toolName: toolName, args: args})
|
|
text := `{}`
|
|
if c.resolveUsers && toolName == "get_user_info_by_user_ids" {
|
|
text = `{"result":[{"userId":"123","openDingTalkId":"open-123"}]}`
|
|
}
|
|
return &edition.ToolResult{Content: []edition.ContentBlock{{Type: "text", Text: text}}}, nil
|
|
}
|
|
|
|
func (*chatChangedContractCaller) Format() string { return "json" }
|
|
func (*chatChangedContractCaller) DryRun() bool { return false }
|
|
func (*chatChangedContractCaller) Fields() string { return "" }
|
|
func (*chatChangedContractCaller) JQ() string { return "" }
|
|
|
|
func executeChatChangedContract(t *testing.T, caller *chatChangedContractCaller, args ...string) error {
|
|
t.Helper()
|
|
previousDeps := deps
|
|
t.Cleanup(func() { deps = previousDeps })
|
|
InitDeps(caller)
|
|
deps.Out.w = io.Discard
|
|
cmd := newChatCommand()
|
|
cmd.SilenceErrors = true
|
|
cmd.SilenceUsage = true
|
|
cmd.SetArgs(args)
|
|
return cmd.Execute()
|
|
}
|
|
|
|
func TestCrossPlatformCoverageChatMessageListUsesMCPMetadataGroupKey(t *testing.T) {
|
|
caller := &chatChangedContractCaller{}
|
|
err := executeChatChangedContract(t, caller,
|
|
"message", "list", "--group", "cid-1", "--time", "2026-07-15 09:00:00", "--limit", "50")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(caller.calls) != 1 || caller.calls[0].toolName != "list_conversation_message_v2" {
|
|
t.Fatalf("calls = %#v", caller.calls)
|
|
}
|
|
want := map[string]any{"openconversation_id": "cid-1", "time": "2026-07-15 09:00:00", "forward": true, "limit": 50}
|
|
if !reflect.DeepEqual(caller.calls[0].args, want) {
|
|
t.Fatalf("tool args = %#v, want %#v", caller.calls[0].args, want)
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageChatAuditUsesUserIDs(t *testing.T) {
|
|
caller := &chatChangedContractCaller{}
|
|
err := executeChatChangedContract(t, caller,
|
|
"group", "audit-join-validation",
|
|
"--group", "cid-1", "--record-id", "123", "--applicant", "user-a", "--inviter", "user-b", "--status", "AuditApprove")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(caller.calls) != 1 || caller.calls[0].productID != "im" || caller.calls[0].toolName != "audit_join_group" {
|
|
t.Fatalf("calls = %#v", caller.calls)
|
|
}
|
|
want := map[string]any{
|
|
"openConversationId": "cid-1", "applyRecordId": int64(123),
|
|
"applicantUid": "user-a", "inviterUid": "user-b", "status": "AuditApprove",
|
|
}
|
|
if !reflect.DeepEqual(caller.calls[0].args, want) {
|
|
t.Fatalf("tool args = %#v, want %#v", caller.calls[0].args, want)
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageChatSendResolvesUserBeforeDispatch(t *testing.T) {
|
|
caller := &chatChangedContractCaller{resolveUsers: true}
|
|
err := executeChatChangedContract(t, caller, "message", "send", "--user", "123", "--text", "hello")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(caller.calls) != 2 || caller.calls[1].toolName != "send_personal_message" {
|
|
t.Fatalf("calls = %#v", caller.calls)
|
|
}
|
|
if got := caller.calls[1].args["receiverOpenDingTalkId"]; got != "open-123" {
|
|
t.Fatalf("receiverOpenDingTalkId = %#v, args = %#v", got, caller.calls[1].args)
|
|
}
|
|
if _, leaked := caller.calls[1].args["receiverUid"]; leaked {
|
|
t.Fatalf("resolved send must not include receiverUid: %#v", caller.calls[1].args)
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageChatSendFailsClosedWhenUserCannotResolve(t *testing.T) {
|
|
caller := &chatChangedContractCaller{}
|
|
err := executeChatChangedContract(t, caller, "message", "send", "--user", "123", "--text", "hello")
|
|
if err == nil || !strings.Contains(err.Error(), "pass --open-dingtalk-id instead") {
|
|
t.Fatalf("error = %v, want explicit resolution failure", err)
|
|
}
|
|
for _, call := range caller.calls {
|
|
if call.toolName == "send_personal_message" {
|
|
t.Fatalf("unresolved user must not be dispatched: %#v", caller.calls)
|
|
}
|
|
}
|
|
}
|