561 lines
21 KiB
Go
561 lines
21 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 (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"os"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
|
|
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition"
|
|
)
|
|
|
|
type imReadResultCall struct {
|
|
productID string
|
|
toolName string
|
|
}
|
|
|
|
type imReadResultCaller struct {
|
|
responses map[string]string
|
|
results map[string]*edition.ToolResult
|
|
errors map[string]error
|
|
calls []imReadResultCall
|
|
args map[string]any
|
|
dryRun bool
|
|
}
|
|
|
|
func (c *imReadResultCaller) CallTool(_ context.Context, productID, toolName string, args map[string]any) (*edition.ToolResult, error) {
|
|
c.calls = append(c.calls, imReadResultCall{productID: productID, toolName: toolName})
|
|
c.args = map[string]any{}
|
|
for k, v := range args {
|
|
c.args[k] = v
|
|
}
|
|
if err := c.errors[toolName]; err != nil {
|
|
return nil, err
|
|
}
|
|
if result, ok := c.results[toolName]; ok {
|
|
return result, nil
|
|
}
|
|
return &edition.ToolResult{Content: []edition.ContentBlock{{Type: "text", Text: c.responses[toolName]}}}, nil
|
|
}
|
|
|
|
func TestCrossPlatformCoverageChatMessageReadsPreserveToolOperationOnCallerError(t *testing.T) {
|
|
caller := &imReadResultCaller{errors: map[string]error{
|
|
"list_conversation_message_v2": errors.New("dial tcp: connection refused"),
|
|
}}
|
|
|
|
got, err := executeIMReadCommand(t, caller, []string{"dws", "chat"}, newChatCommand,
|
|
"message", "list", "--group", "cid-1", "--time", "2026-07-14 00:00:00")
|
|
if err == nil {
|
|
t.Fatalf("chat message list unexpectedly succeeded with output %q", got)
|
|
}
|
|
if got != "" {
|
|
t.Fatalf("chat message list output = %q, want no success payload", got)
|
|
}
|
|
var cliErr *CLIError
|
|
if !errors.As(err, &cliErr) || cliErr.Operation != "chat/list_conversation_message_v2" {
|
|
t.Fatalf("chat message list error = %#v", err)
|
|
}
|
|
|
|
existing := &CLIError{
|
|
Code: CodeNetworkTimeout,
|
|
Message: "request already classified",
|
|
Operation: "chat/existing-operation",
|
|
}
|
|
caller = &imReadResultCaller{errors: map[string]error{
|
|
"list_conversation_message_v2": existing,
|
|
}}
|
|
got, err = executeIMReadCommand(t, caller, []string{"dws", "chat"}, newChatCommand,
|
|
"message", "list", "--group", "cid-1", "--time", "2026-07-14 00:00:00")
|
|
if err != existing {
|
|
t.Fatalf("chat message list error = %#v, want original %#v", err, existing)
|
|
}
|
|
if got != "" {
|
|
t.Fatalf("chat message list output = %q, want no success payload", got)
|
|
}
|
|
}
|
|
|
|
func (*imReadResultCaller) Format() string { return "json" }
|
|
func (c *imReadResultCaller) DryRun() bool { return c.dryRun }
|
|
func (*imReadResultCaller) Fields() string { return "" }
|
|
func (*imReadResultCaller) JQ() string { return "" }
|
|
|
|
func executeIMReadCommand(t *testing.T, caller *imReadResultCaller, processArgs []string, build func() *cobra.Command, args ...string) (string, error) {
|
|
t.Helper()
|
|
previousDeps := deps
|
|
previousArgs := os.Args
|
|
t.Cleanup(func() {
|
|
deps = previousDeps
|
|
os.Args = previousArgs
|
|
})
|
|
|
|
InitDeps(caller)
|
|
var stdout bytes.Buffer
|
|
deps.Out.w = &stdout
|
|
deps.Out.errW = io.Discard
|
|
os.Args = processArgs
|
|
|
|
root := build()
|
|
installExampleGlobalFlags(root)
|
|
root.SetOut(&stdout)
|
|
root.SilenceErrors = true
|
|
root.SilenceUsage = true
|
|
root.SetArgs(args)
|
|
err := root.Execute()
|
|
return stdout.String(), err
|
|
}
|
|
|
|
func requireSameJSON(t *testing.T, got, want string) {
|
|
t.Helper()
|
|
var gotValue any
|
|
if err := json.Unmarshal([]byte(got), &gotValue); err != nil {
|
|
t.Fatalf("decode command output: %v\noutput: %s", err, got)
|
|
}
|
|
var wantValue any
|
|
if err := json.Unmarshal([]byte(want), &wantValue); err != nil {
|
|
t.Fatalf("decode fixture: %v", err)
|
|
}
|
|
if !reflect.DeepEqual(gotValue, wantValue) {
|
|
t.Fatalf("command output = %#v, want %#v", gotValue, wantValue)
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageChatMessageListProjectsStableFieldsAndPreservesLegacy(t *testing.T) {
|
|
payload := `{
|
|
"result": {
|
|
"messages": [
|
|
{"openMessageId":"msg-1","content":{"text":"回复正文"},"createTime":101,"msgType":"reply","quotedMessage":{"msgType":"merged_forward","content":{"items":[{"text":"原消息"}]}}},
|
|
{"openMessageId":"msg-2","content":{"text":"图片回复"},"createTime":102,"msgType":"reply","quotedMessage":{"msgType":"image","content":{"mediaId":"media-1"}}}
|
|
],
|
|
"hasMore": false
|
|
}
|
|
}`
|
|
caller := &imReadResultCaller{responses: map[string]string{"list_conversation_message_v2": payload}}
|
|
|
|
got, err := executeIMReadCommand(t, caller, []string{"dws", "chat"}, newChatCommand,
|
|
"message", "list", "--group", "cid-1", "--time", "2026-07-14 00:00:00", "--limit", "50")
|
|
if err != nil {
|
|
t.Fatalf("chat message list returned error: %v", err)
|
|
}
|
|
if len(caller.calls) != 1 || caller.calls[0] != (imReadResultCall{productID: "chat", toolName: "list_conversation_message_v2"}) {
|
|
t.Fatalf("calls = %#v, want chat/list_conversation_message_v2", caller.calls)
|
|
}
|
|
var result map[string]any
|
|
if err := json.Unmarshal([]byte(got), &result); err != nil {
|
|
t.Fatalf("decode command output: %v\noutput: %s", err, got)
|
|
}
|
|
if _, exists := result["contractVersion"]; exists {
|
|
t.Fatalf("typed list added an out-of-scope contract envelope: %#v", result)
|
|
}
|
|
messages, ok := result["messages"].([]any)
|
|
if !ok || len(messages) != 2 {
|
|
t.Fatalf("messages = %#v", result["messages"])
|
|
}
|
|
first, ok := messages[0].(map[string]any)
|
|
if !ok || first["messageId"] != "msg-1" || first["openMessageId"] != "msg-1" || first["text"] != "回复正文" {
|
|
t.Fatalf("first projected message = %#v", messages[0])
|
|
}
|
|
if _, ok := first["content"].(map[string]any); !ok {
|
|
t.Fatalf("legacy content not preserved: %#v", first)
|
|
}
|
|
quoted, ok := first["quotedMessage"].(map[string]any)
|
|
if !ok || quoted["msgType"] != "merged_forward" {
|
|
t.Fatalf("merged-forward quote = %#v", first["quotedMessage"])
|
|
}
|
|
quotedContent, ok := quoted["content"].(map[string]any)
|
|
items, itemsOK := quotedContent["items"].([]any)
|
|
if !ok || !itemsOK || len(items) != 1 {
|
|
t.Fatalf("merged-forward quote content = %#v", quoted["content"])
|
|
}
|
|
second, ok := messages[1].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("second projected message = %#v", messages[1])
|
|
}
|
|
quoted, ok = second["quotedMessage"].(map[string]any)
|
|
if !ok || quoted["msgType"] != "image" {
|
|
t.Fatalf("image quote = %#v", second["quotedMessage"])
|
|
}
|
|
quotedContent, ok = quoted["content"].(map[string]any)
|
|
if !ok || quotedContent["mediaId"] != "media-1" {
|
|
t.Fatalf("image quote content = %#v", quoted["content"])
|
|
}
|
|
legacy, ok := result["result"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("legacy result envelope missing: %#v", result["result"])
|
|
}
|
|
legacyMessages, ok := legacy["messages"].([]any)
|
|
if !ok || len(legacyMessages) != 2 {
|
|
t.Fatalf("legacy result.messages = %#v", legacy["messages"])
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageChatMessageListDefaultsTimeAndOlderDirection(t *testing.T) {
|
|
payload := `{"result":{"messages":[],"hasMore":false}}`
|
|
caller := &imReadResultCaller{responses: map[string]string{"list_conversation_message_v2": payload}}
|
|
loc := shanghaiLocation()
|
|
|
|
before := time.Now().In(loc)
|
|
_, err := executeIMReadCommand(t, caller, []string{"dws", "chat"}, newChatCommand,
|
|
"message", "list", "--group", "cid-1", "--limit", "50")
|
|
after := time.Now().In(loc)
|
|
if err != nil {
|
|
t.Fatalf("chat message list returned error: %v", err)
|
|
}
|
|
if len(caller.calls) != 1 || caller.calls[0] != (imReadResultCall{productID: "chat", toolName: "list_conversation_message_v2"}) {
|
|
t.Fatalf("calls = %#v, want chat/list_conversation_message_v2", caller.calls)
|
|
}
|
|
if got := caller.args["forward"]; got != false {
|
|
t.Fatalf("forward = %#v, want false when --time is omitted", got)
|
|
}
|
|
timeRaw, ok := caller.args["time"].(string)
|
|
if !ok || strings.TrimSpace(timeRaw) == "" {
|
|
t.Fatalf("time arg = %#v, want generated current time", caller.args["time"])
|
|
}
|
|
gotTime, err := time.ParseInLocation("2006-01-02 15:04:05", timeRaw, loc)
|
|
if err != nil {
|
|
t.Fatalf("parse generated time %q: %v", timeRaw, err)
|
|
}
|
|
if gotTime.Before(before.Add(-time.Second)) || gotTime.After(after.Add(time.Second)) {
|
|
t.Fatalf("time = %s, want between %s and %s", gotTime, before, after)
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageChatMessageSearchProjectsStableFieldsAndPreservesLegacy(t *testing.T) {
|
|
payload := `{
|
|
"result": {
|
|
"conversationMessagesList": [{
|
|
"openConversationId": "cid-1",
|
|
"title": "项目群",
|
|
"messages": [{"openMessageId":"msg-search-1","messageId":"legacy-conflict","content":{"richText":"发布计划"},"createTime":201}]
|
|
}],
|
|
"nextCursor": "cursor-2"
|
|
}
|
|
}`
|
|
caller := &imReadResultCaller{responses: map[string]string{"search_messages_by_keyword": payload}}
|
|
|
|
got, err := executeIMReadCommand(t, caller, []string{"dws", "chat"}, newChatCommand,
|
|
"message", "search", "--query", "发布计划",
|
|
"--start", "2026-07-01T00:00:00+08:00", "--end", "2026-07-10T00:00:00+08:00")
|
|
if err != nil {
|
|
t.Fatalf("chat message search returned error: %v", err)
|
|
}
|
|
if len(caller.calls) != 1 || caller.calls[0] != (imReadResultCall{productID: "chat", toolName: "search_messages_by_keyword"}) {
|
|
t.Fatalf("calls = %#v, want chat/search_messages_by_keyword", caller.calls)
|
|
}
|
|
var result map[string]any
|
|
if err := json.Unmarshal([]byte(got), &result); err != nil {
|
|
t.Fatalf("decode command output: %v\noutput: %s", err, got)
|
|
}
|
|
if _, exists := result["contractVersion"]; exists {
|
|
t.Fatalf("typed search added an out-of-scope contract envelope: %#v", result)
|
|
}
|
|
messages, ok := result["messages"].([]any)
|
|
if !ok || len(messages) != 1 {
|
|
t.Fatalf("messages = %#v", result["messages"])
|
|
}
|
|
message, ok := messages[0].(map[string]any)
|
|
if !ok || message["messageId"] != "msg-search-1" || message["openMessageId"] != "msg-search-1" ||
|
|
message["conversationId"] != "cid-1" || message["text"] != "发布计划" {
|
|
t.Fatalf("projected search message = %#v", messages[0])
|
|
}
|
|
if _, ok := message["content"].(map[string]any); !ok {
|
|
t.Fatalf("legacy content not preserved: %#v", message)
|
|
}
|
|
legacy, ok := result["result"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("legacy result envelope missing: %#v", result["result"])
|
|
}
|
|
legacyGroups, ok := legacy["conversationMessagesList"].([]any)
|
|
if !ok || len(legacyGroups) != 1 {
|
|
t.Fatalf("legacy result.conversationMessagesList = %#v", legacy["conversationMessagesList"])
|
|
}
|
|
if legacy["nextCursor"] != "cursor-2" {
|
|
t.Fatalf("legacy result.nextCursor = %#v", legacy["nextCursor"])
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageChatMessageScopedSearchProjectsStableFields(t *testing.T) {
|
|
payload := `{
|
|
"result": {
|
|
"conversationMessagesList": [{
|
|
"openConversationId": "cid-1",
|
|
"messages": [{"openMessageId":"msg-scoped-1","content":{"text":"范围内消息"}}]
|
|
}],
|
|
"hasMore": false
|
|
}
|
|
}`
|
|
caller := &imReadResultCaller{responses: map[string]string{
|
|
"get_conversation_info": `{}`,
|
|
"search_messages_by_keyword": payload,
|
|
}}
|
|
|
|
got, err := executeIMReadCommand(t, caller, []string{"dws", "chat"}, newChatCommand,
|
|
"message", "search", "--query", "范围内消息", "--group", "cid-1",
|
|
"--start", "2026-07-01T00:00:00+08:00", "--end", "2026-07-10T00:00:00+08:00")
|
|
if err != nil {
|
|
t.Fatalf("scoped chat message search returned error: %v", err)
|
|
}
|
|
wantCalls := []imReadResultCall{
|
|
{productID: "chat", toolName: "get_conversation_info"},
|
|
{productID: "chat", toolName: "search_messages_by_keyword"},
|
|
}
|
|
if !reflect.DeepEqual(caller.calls, wantCalls) {
|
|
t.Fatalf("calls = %#v, want %#v", caller.calls, wantCalls)
|
|
}
|
|
|
|
var result map[string]any
|
|
if err := json.Unmarshal([]byte(got), &result); err != nil {
|
|
t.Fatalf("decode command output: %v\noutput: %s", err, got)
|
|
}
|
|
messages, ok := result["messages"].([]any)
|
|
if !ok || len(messages) != 1 {
|
|
t.Fatalf("messages = %#v", result["messages"])
|
|
}
|
|
message, _ := messages[0].(map[string]any)
|
|
if message["messageId"] != "msg-scoped-1" || message["openMessageId"] != "msg-scoped-1" ||
|
|
message["conversationId"] != "cid-1" || message["text"] != "范围内消息" {
|
|
t.Fatalf("projected scoped message = %#v", message)
|
|
}
|
|
scope, _ := result["scope"].(map[string]any)
|
|
if scope["targetsValidated"] != true || scope["resultsWithinScope"] != true || scope["sourceComplete"] != true {
|
|
t.Fatalf("scope = %#v", scope)
|
|
}
|
|
legacy, _ := result["result"].(map[string]any)
|
|
if _, ok := legacy["conversationMessagesList"].([]any); !ok {
|
|
t.Fatalf("legacy result envelope missing: %#v", legacy)
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageChatMessageListPreservesNonJSONResponse(t *testing.T) {
|
|
const payload = "upstream temporarily returned plain text"
|
|
caller := &imReadResultCaller{responses: map[string]string{"list_conversation_message_v2": payload}}
|
|
|
|
got, err := executeIMReadCommand(t, caller, []string{"dws", "chat"}, newChatCommand,
|
|
"message", "list", "--group", "cid-1", "--time", "2026-07-14 00:00:00")
|
|
if err != nil {
|
|
t.Fatalf("chat message list returned error: %v", err)
|
|
}
|
|
if got != payload+"\n" {
|
|
t.Fatalf("command output = %q, want raw payload", got)
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageChatMessageReadsRejectEmptyToolResults(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
toolName string
|
|
caller *imReadResultCaller
|
|
args []string
|
|
}{
|
|
{
|
|
name: "list empty text",
|
|
toolName: "list_conversation_message_v2",
|
|
caller: &imReadResultCaller{responses: map[string]string{
|
|
"list_conversation_message_v2": " \n ",
|
|
}},
|
|
args: []string{"message", "list", "--group", "cid-1", "--time", "2026-07-14 00:00:00"},
|
|
},
|
|
{
|
|
name: "search without text content",
|
|
toolName: "search_messages_by_keyword",
|
|
caller: &imReadResultCaller{results: map[string]*edition.ToolResult{
|
|
"search_messages_by_keyword": {Content: []edition.ContentBlock{{Type: "image"}}},
|
|
}},
|
|
args: []string{"message", "search", "--query", "发布计划",
|
|
"--start", "2026-07-01T00:00:00+08:00", "--end", "2026-07-10T00:00:00+08:00"},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
got, err := executeIMReadCommand(t, tc.caller, []string{"dws", "chat"}, newChatCommand, tc.args...)
|
|
if err == nil {
|
|
t.Fatalf("%s unexpectedly succeeded with output %q", tc.toolName, got)
|
|
}
|
|
if got != "" {
|
|
t.Fatalf("%s output = %q, want no success payload", tc.toolName, got)
|
|
}
|
|
var apiErr *apperrors.Error
|
|
if !errors.As(err, &apiErr) || apiErr.Reason != "empty_tool_response" ||
|
|
apiErr.Operation != "chat/"+tc.toolName || !apiErr.RetryableSet || !apiErr.Retryable {
|
|
t.Fatalf("%s error = %#v", tc.toolName, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageChatMessageListPreservesTopLevelMessageFields(t *testing.T) {
|
|
payload := `{
|
|
"messages": [{
|
|
"openMessageId": "msg-top-1",
|
|
"content": {"text": "顶层正文"},
|
|
"msgType": "text",
|
|
"sender": {"name": "张三", "department": "研发"},
|
|
"senderName": "张三",
|
|
"quotedMessage": {
|
|
"msgType": "image",
|
|
"content": {"mediaId": "media-1", "caption": "原图说明"},
|
|
"extension": {"source": "legacy-quote"}
|
|
},
|
|
"forwarded": [{
|
|
"openMessageId": "forward-1",
|
|
"content": {"text": "转发正文"},
|
|
"extension": {"source": "legacy-forward"}
|
|
}],
|
|
"reactions": [{
|
|
"emoji": "like",
|
|
"count": 2,
|
|
"extension": {"source": "legacy-reaction"}
|
|
}],
|
|
"extensionField": {"source": "legacy"}
|
|
}, {
|
|
"messageId": "msg-top-2",
|
|
"text": "稳定正文"
|
|
}],
|
|
"count": 99,
|
|
"partial": true,
|
|
"failedCount": 1,
|
|
"failures": [{"stage":"legacy","error":"legacy failure"}],
|
|
"hasMore": false
|
|
}`
|
|
caller := &imReadResultCaller{responses: map[string]string{"list_conversation_message_v2": payload}}
|
|
|
|
got, err := executeIMReadCommand(t, caller, []string{"dws", "chat"}, newChatCommand,
|
|
"message", "list", "--group", "cid-1", "--time", "2026-07-14 00:00:00")
|
|
if err != nil {
|
|
t.Fatalf("chat message list returned error: %v", err)
|
|
}
|
|
var result map[string]any
|
|
if err := json.Unmarshal([]byte(got), &result); err != nil {
|
|
t.Fatalf("decode command output: %v\noutput: %s", err, got)
|
|
}
|
|
messages, ok := result["messages"].([]any)
|
|
if !ok || len(messages) != 2 {
|
|
t.Fatalf("messages = %#v", result["messages"])
|
|
}
|
|
message, ok := messages[0].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("message = %#v", messages[0])
|
|
}
|
|
if message["messageId"] != "msg-top-1" || message["text"] != "顶层正文" {
|
|
t.Fatalf("stable fields = %#v", message)
|
|
}
|
|
if message["msgType"] != "text" || message["senderName"] != "张三" {
|
|
t.Fatalf("legacy message fields = %#v", message)
|
|
}
|
|
if !reflect.DeepEqual(message["sender"], map[string]any{"name": "张三", "department": "研发"}) {
|
|
t.Fatalf("legacy sender = %#v", message["sender"])
|
|
}
|
|
wantQuoted := map[string]any{
|
|
"msgType": "image",
|
|
"content": map[string]any{"mediaId": "media-1", "caption": "原图说明"},
|
|
"extension": map[string]any{"source": "legacy-quote"},
|
|
}
|
|
if !reflect.DeepEqual(message["quotedMessage"], wantQuoted) {
|
|
t.Fatalf("legacy quotedMessage = %#v", message["quotedMessage"])
|
|
}
|
|
wantForwarded := []any{map[string]any{
|
|
"openMessageId": "forward-1",
|
|
"content": map[string]any{"text": "转发正文"},
|
|
"extension": map[string]any{"source": "legacy-forward"},
|
|
}}
|
|
if !reflect.DeepEqual(message["forwarded"], wantForwarded) {
|
|
t.Fatalf("legacy forwarded = %#v", message["forwarded"])
|
|
}
|
|
wantReactions := []any{map[string]any{
|
|
"emoji": "like",
|
|
"count": float64(2),
|
|
"extension": map[string]any{"source": "legacy-reaction"},
|
|
}}
|
|
if !reflect.DeepEqual(message["reactions"], wantReactions) {
|
|
t.Fatalf("legacy reactions = %#v", message["reactions"])
|
|
}
|
|
if extension, ok := message["extensionField"].(map[string]any); !ok || extension["source"] != "legacy" {
|
|
t.Fatalf("extensionField = %#v", message["extensionField"])
|
|
}
|
|
stableOnly, ok := messages[1].(map[string]any)
|
|
if !ok || stableOnly["openMessageId"] != "msg-top-2" || stableOnly["content"] != "稳定正文" {
|
|
t.Fatalf("legacy aliases from stable fields = %#v", messages[1])
|
|
}
|
|
if result["count"] != float64(99) || result["partial"] != true || result["failedCount"] != float64(1) {
|
|
t.Fatalf("legacy top-level envelope fields = %#v", result)
|
|
}
|
|
if _, exists := result["contractVersion"]; exists {
|
|
t.Fatalf("typed list added an out-of-scope contract envelope: %#v", result)
|
|
}
|
|
failures, ok := result["failures"].([]any)
|
|
if !ok || len(failures) != 1 || failures[0].(map[string]any)["stage"] != "legacy" {
|
|
t.Fatalf("legacy failures = %#v", result["failures"])
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageChatMessageListDryRunKeepsPreviewPath(t *testing.T) {
|
|
caller := &imReadResultCaller{dryRun: true}
|
|
|
|
got, err := executeIMReadCommand(t, caller, []string{"dws", "chat"}, newChatCommand,
|
|
"message", "list", "--group", "cid-1", "--time", "2026-07-14 00:00:00")
|
|
if err != nil {
|
|
t.Fatalf("chat message list dry-run returned error: %v", err)
|
|
}
|
|
if len(caller.calls) != 0 {
|
|
t.Fatalf("dry-run calls = %#v, want none", caller.calls)
|
|
}
|
|
var preview map[string]any
|
|
if err := json.Unmarshal([]byte(got), &preview); err != nil {
|
|
t.Fatalf("decode dry-run output: %v\noutput: %s", err, got)
|
|
}
|
|
if preview["dry_run"] != true || preview["executed"] != false || preview["tool"] != "list_conversation_message_v2" {
|
|
t.Fatalf("dry-run preview = %#v", preview)
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageChatMessageListRawPreservesLargeIntegers(t *testing.T) {
|
|
const payload = `{"messages":[{"openMessageId":"msg-1","content":{"text":"正文"},"sequence":9007199254740993}],"hasMore":false}`
|
|
caller := &imReadResultCaller{responses: map[string]string{"list_conversation_message_v2": payload}}
|
|
|
|
got, err := executeIMReadCommand(t, caller, []string{"dws", "chat"}, newChatCommand,
|
|
"message", "list", "--group", "cid-1", "--time", "2026-07-14 00:00:00", "--format", "raw")
|
|
if err != nil {
|
|
t.Fatalf("chat message list returned error: %v", err)
|
|
}
|
|
if !bytes.Contains([]byte(got), []byte(`"sequence":9007199254740993`)) {
|
|
t.Fatalf("raw output changed large integer: %s", got)
|
|
}
|
|
}
|
|
|
|
func TestDingMessageListPreservesContent(t *testing.T) {
|
|
payload := `{"result":{"dingMessages":[{"openDingId":"ding-1","status":"READ","content":"升级提醒"}]}}`
|
|
caller := &imReadResultCaller{responses: map[string]string{"list_ding_messages": payload}}
|
|
|
|
got, err := executeIMReadCommand(t, caller, []string{"dws", "ding"}, newDingCommand,
|
|
"message", "list", "--type", "ALL")
|
|
if err != nil {
|
|
t.Fatalf("ding message list returned error: %v", err)
|
|
}
|
|
if len(caller.calls) != 1 || caller.calls[0] != (imReadResultCall{productID: "im", toolName: "list_ding_messages"}) {
|
|
t.Fatalf("calls = %#v, want im/list_ding_messages", caller.calls)
|
|
}
|
|
requireSameJSON(t, got, payload)
|
|
}
|