From 56bb50913b2f5094525d96ca343cc898ff722a60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=95=BF=E7=9C=9F?= Date: Wed, 12 Aug 2026 16:23:13 +0800 Subject: [PATCH 01/16] feat(chat): default message query time ranges --- internal/helpers/chat.go | 112 +++++++++--------- internal/helpers/chat_message_help_test.go | 93 +++++++++++++++ .../helpers/chat_message_pagination_test.go | 84 +++++++++++++ .../helpers/im_read_result_contract_test.go | 38 +++++- test/fixtures/cli-interface-baseline.txt | 8 +- 5 files changed, 275 insertions(+), 60 deletions(-) diff --git a/internal/helpers/chat.go b/internal/helpers/chat.go index 3bf56fe6..3cc7a516 100644 --- a/internal/helpers/chat.go +++ b/internal/helpers/chat.go @@ -467,22 +467,17 @@ func validateNativeSearchConversationScope(conversationIDs []string) error { } func chatMessageListAllArgs(cmd *cobra.Command) (map[string]any, error) { - if err := validateRequiredFlags(cmd, "start", "end"); err != nil { - return nil, err - } + startRaw, endRaw := defaultChatMessageTimeRange(cmd, 24*time.Hour) cursor, _ := cmd.Flags().GetString("cursor") return map[string]any{ - "startTime": mustGetFlag(cmd, "start"), - "endTime": mustGetFlag(cmd, "end"), + "startTime": startRaw, + "endTime": endRaw, "limit": chatIntFlagOrFallback(cmd, "limit", "size"), "cursor": cursor, }, nil } func chatMessageListBySenderArgs(cmd *cobra.Command) (map[string]any, error) { - if err := validateRequiredFlags(cmd, "start"); err != nil { - return nil, err - } senderUserID := flagOrFallback(cmd, "sender-user-id", "sender") senderOpenDingTalkID, _ := cmd.Flags().GetString("sender-open-dingtalk-id") if senderUserID != "" && senderOpenDingTalkID != "" { @@ -491,14 +486,11 @@ func chatMessageListBySenderArgs(cmd *cobra.Command) (map[string]any, error) { if senderUserID == "" && senderOpenDingTalkID == "" { return nil, fmt.Errorf("--sender-user-id or --sender-open-dingtalk-id is required") } - startMs, err := parseISOTimeToMillis("start", mustGetFlag(cmd, "start")) + startRaw, endRaw := defaultChatMessageTimeRange(cmd, 7*24*time.Hour) + startMs, err := parseISOTimeToMillis("start", startRaw) if err != nil { return nil, err } - endRaw, _ := cmd.Flags().GetString("end") - if strings.TrimSpace(endRaw) == "" { - endRaw = time.Now().Format(time.RFC3339) - } endMs, err := parseISOTimeToMillis("end", endRaw) if err != nil { return nil, err @@ -522,14 +514,12 @@ func chatMessageListBySenderArgs(cmd *cobra.Command) (map[string]any, error) { } func chatMessageListMentionsArgs(cmd *cobra.Command) (map[string]any, error) { - if err := validateRequiredFlags(cmd, "start", "end"); err != nil { - return nil, err - } - startMs, err := parseISOTimeToMillis("start", mustGetFlag(cmd, "start")) + startRaw, endRaw := defaultChatMessageTimeRange(cmd, 7*24*time.Hour) + startMs, err := parseISOTimeToMillis("start", startRaw) if err != nil { return nil, err } - endMs, err := parseISOTimeToMillis("end", mustGetFlag(cmd, "end")) + endMs, err := parseISOTimeToMillis("end", endRaw) if err != nil { return nil, err } @@ -564,14 +554,12 @@ func chatMessageSearchArgs(cmd *cobra.Command) (map[string]any, error) { if err := validateRequiredFlagWithAliases(cmd, "query", "keyword"); err != nil { return nil, err } - if err := validateRequiredFlags(cmd, "start", "end"); err != nil { - return nil, err - } - startMs, err := parseISOTimeToMillis("start", mustGetFlag(cmd, "start")) + startRaw, endRaw := defaultChatMessageTimeRange(cmd, 7*24*time.Hour) + startMs, err := parseISOTimeToMillis("start", startRaw) if err != nil { return nil, err } - endMs, err := parseISOTimeToMillis("end", mustGetFlag(cmd, "end")) + endMs, err := parseISOTimeToMillis("end", endRaw) if err != nil { return nil, err } @@ -592,6 +580,19 @@ func chatMessageSearchArgs(cmd *cobra.Command) (map[string]any, error) { return toolArgs, nil } +func defaultChatMessageTimeRange(cmd *cobra.Command, lookback time.Duration) (string, string) { + startRaw := mustGetFlag(cmd, "start") + endRaw := mustGetFlag(cmd, "end") + now := time.Now() + if endRaw == "" { + endRaw = now.Format(time.RFC3339) + } + if startRaw == "" { + startRaw = now.Add(-lookback).Format(time.RFC3339) + } + return startRaw, endRaw +} + func chatMessageSearchAdvancedArgs(cmd *cobra.Command) (map[string]any, error) { toolArgs := map[string]any{} if v := flagOrFallback(cmd, "query", "keyword"); v != "" { @@ -2605,8 +2606,9 @@ func newChatCommand() *cobra.Command { chatMessageListCmd := &cobra.Command{ Use: "list", Short: "拉取会话消息内容", - Long: `拉取指定群聊或单聊的会话消息内容。输出顶层 messages,稳定字段为 messageId 和 text;兼容保留 openMessageId 和 content。--group 指定群聊,--user 指定单聊用户(userId),--open-dingtalk-id 指定单聊用户(openDingTalkId),三者互斥。推荐使用 --direction newer/older 控制时间方向:newer 表示从给定时间往现在拉,older 表示从给定时间往以前拉。hasMore=true 时用结果中的边界 createTime 作为下次 --time 翻页。引用回复消息会返回 quotedMessage 引用上下文;被引用的原消息是合并转发或图片时,对应的类型与内容也会随引用上下文返回。如果返回的会话消息中包含 openConvThreadId 字段,说明是话题消息,可以调用 dws chat message list-topic-replies 拉取话题回复消息列表,openConvThreadId 作为 topic-id 参数。`, - Example: ` dws chat message list --group --time "2025-03-01 00:00:00" + Long: `拉取指定群聊或单聊的会话消息内容。输出顶层 messages,稳定字段为 messageId 和 text;兼容保留 openMessageId 和 content。--group 指定群聊,--user 指定单聊用户(userId),--open-dingtalk-id 指定单聊用户(openDingTalkId),三者互斥。--time 可选,不传时默认当前时间并向旧消息拉取。推荐使用 --direction newer/older 控制时间方向:newer 表示从给定时间往现在拉,older 表示从给定时间往以前拉。hasMore=true 时用结果中的边界 createTime 作为下次 --time 翻页。引用回复消息会返回 quotedMessage 引用上下文;被引用的原消息是合并转发或图片时,对应的类型与内容也会随引用上下文返回。如果返回的会话消息中包含 openConvThreadId 字段,说明是话题消息,可以调用 dws chat message list-topic-replies 拉取话题回复消息列表,openConvThreadId 作为 topic-id 参数。`, + Example: ` dws chat message list --group + dws chat message list --group --time "2025-03-01 00:00:00" dws chat message list --user --time "2025-03-01 00:00:00" --limit 50 dws chat message list --open-dingtalk-id --time "2025-03-01 00:00:00" --limit 50 dws chat message list --group --time "2025-03-01 00:00:00" --direction older @@ -2614,9 +2616,6 @@ func newChatCommand() *cobra.Command { # 查询群 ID: dws chat search --query "群名" # 查询 userId: dws contact user search --query "姓名"`, RunE: func(cmd *cobra.Command, args []string) error { - if err := validateRequiredFlags(cmd, "time"); err != nil { - return err - } groupID := flagOrFallback(cmd, "group", "conversation-id", "id", "chat") userID, _ := cmd.Flags().GetString("user") openDingTalkID, _ := cmd.Flags().GetString("open-dingtalk-id") @@ -2640,11 +2639,16 @@ func newChatCommand() *cobra.Command { openDingTalkID = userID userID = "" } - forward, err := resolveMessageForward(cmd, true) + timeVal := mustGetFlag(cmd, "time") + defaultForward := true + if timeVal == "" { + timeVal = time.Now().Format("2006-01-02 15:04:05") + defaultForward = false + } + forward, err := resolveMessageForward(cmd, defaultForward) if err != nil { return err } - timeVal := mustGetFlag(cmd, "time") if groupID != "" { toolArgs := map[string]any{ "openconversation_id": groupID, @@ -2695,6 +2699,7 @@ func newChatCommand() *cobra.Command { UseWhen: []string{"用户明确指定某个会话,并要读取消息或追溯引用回复中的原消息上下文时"}, AvoidWhen: []string{"跨全部会话按时间查询时使用 chat message list-all"}, Examples: []string{ + "dws chat message list --group --limit 50", "dws chat message list --group --time \"2026-07-01 00:00:00\" --limit 50", "dws chat message list --group --time \"2026-07-01 00:00:00\" --limit 50 --jq '.messages[] | {messageId, text}'", }, @@ -3483,7 +3488,7 @@ func newChatCommand() *cobra.Command { chatMessageListAllCmd := &cobra.Command{ Use: "list-all", Short: "拉取指定时间范围内当前用户的所有会话消息", - Long: `分页拉取当前登录用户在指定时间范围内的所有会话消息。--start 和 --end 限定时间范围,--limit 指定每页数量,--cursor 传分页游标(首页传 0)。服务端按 cursor 分页返回,hasMore=true 时用返回的 nextCursor 值继续翻页。默认只读取单页;只有显式传 --page-all 才会自动翻页并保留、合并 result.conversationMessagesList,同一会话跨页合并 messages。只传 --page-limit、--max-items 或 --page-delay 仍保持单页调用。自动翻页时 --page-limit 控制最多请求页数,--max-items 按消息数精确截断,--page-delay 控制页间等待毫秒数。如果当前账号没有消息搜索权益,CLI 会保留服务端返回的友好提示与开通入口;不要把权限错误解释为时间范围内没有消息。`, + Long: `分页拉取当前登录用户在指定时间范围内的所有会话消息。--start 和 --end 可选,不传时默认最近 1 天到当前时间,避免跨全部会话范围过大;--limit 指定每页数量,--cursor 传分页游标(首页传 0)。服务端按 cursor 分页返回,hasMore=true 时用返回的 nextCursor 值继续翻页。默认只读取单页;只有显式传 --page-all 才会自动翻页并保留、合并 result.conversationMessagesList,同一会话跨页合并 messages。只传 --page-limit、--max-items 或 --page-delay 仍保持单页调用。自动翻页时 --page-limit 控制最多请求页数,--max-items 按消息数精确截断,--page-delay 控制页间等待毫秒数。如果当前账号没有消息搜索权益,CLI 会保留服务端返回的友好提示与开通入口;不要把权限错误解释为时间范围内没有消息。`, Example: ` dws chat message list-all --start "2025-03-01 00:00:00" --end "2025-03-31 23:59:59" --limit 50 dws chat message list-all --start "2025-03-01 00:00:00" --end "2025-03-31 23:59:59" --limit 50 --cursor "abc123token" dws chat message list-all --start "2025-03-01 00:00:00" --end "2025-03-31 23:59:59" --limit 100 --page-all --page-limit 20 --max-items 500 --page-delay 0`, @@ -3530,7 +3535,7 @@ func newChatCommand() *cobra.Command { chatMessageListBySenderCmd := &cobra.Command{ Use: "list-by-sender", Short: "拉取指定发送者的消息(包含单聊和群聊)", - Long: `搜索特定人发送给我的消息,返回结果包含单聊和群聊标识。--sender-user-id 指定发送者 userId,--sender-open-dingtalk-id 指定发送者 openDingTalkId,二者互斥。分页参数 --limit(默认 50)和 --cursor(默认 "0")始终传递;hasMore=true 时用返回的 nextCursor 作为下次 --cursor 继续翻页。默认只读取单页;只有显式传 --page-all 才会自动翻页并保留、合并 result.conversationMessagesList,同一会话跨页合并 messages。只传 --page-limit、--max-items 或 --page-delay 仍保持单页调用。自动翻页时 --page-limit 控制最多请求页数,--max-items 按消息数精确截断,--page-delay 控制页间等待毫秒数。`, + Long: `搜索特定人发送给我的消息,返回结果包含单聊和群聊标识。--sender-user-id 指定发送者 userId,--sender-open-dingtalk-id 指定发送者 openDingTalkId,二者互斥。--start 和 --end 可选,不传时默认最近 7 天到当前时间。分页参数 --limit(默认 50)和 --cursor(默认 "0")始终传递;hasMore=true 时用返回的 nextCursor 作为下次 --cursor 继续翻页。默认只读取单页;只有显式传 --page-all 才会自动翻页并保留、合并 result.conversationMessagesList,同一会话跨页合并 messages。只传 --page-limit、--max-items 或 --page-delay 仍保持单页调用。自动翻页时 --page-limit 控制最多请求页数,--max-items 按消息数精确截断,--page-delay 控制页间等待毫秒数。`, Example: ` dws chat message list-by-sender --sender-user-id --start "2026-03-10T00:00:00+08:00" --end "2026-03-11T00:00:00+08:00" --limit 50 --cursor 0 dws chat message list-by-sender --sender-open-dingtalk-id --start "2026-03-10T00:00:00+08:00" --end "2026-03-11T00:00:00+08:00" --limit 50 --cursor 0 dws chat message list-by-sender --sender-user-id --start "2026-03-10T00:00:00+08:00" --end "2026-03-10T23:59:59+08:00" --limit 20 --cursor 0 @@ -3582,7 +3587,7 @@ func newChatCommand() *cobra.Command { chatMessageListMentionsCmd := &cobra.Command{ Use: "list-mentions", Short: "拉取 @我 的消息", - Long: `搜索时间范围内 @我 的消息,可选指定群聊。返回结果包含单聊和群聊标识。分页参数 --limit(默认 50)和 --cursor(默认 "0")始终传递;hasMore=true 时用返回的 nextCursor 作为下次 --cursor 继续翻页。默认只读取单页;只有显式传 --page-all 才会自动翻页并保留、合并 result.conversationMessagesList,同一会话跨页合并 messages。只传 --page-limit、--max-items 或 --page-delay 仍保持单页调用。自动翻页时 --page-limit 控制最多请求页数,--max-items 按消息数精确截断,--page-delay 控制页间等待毫秒数。`, + Long: `搜索时间范围内 @我 的消息,可选指定群聊。--start 和 --end 可选,不传时默认最近 7 天到当前时间。返回结果包含单聊和群聊标识。分页参数 --limit(默认 50)和 --cursor(默认 "0")始终传递;hasMore=true 时用返回的 nextCursor 作为下次 --cursor 继续翻页。默认只读取单页;只有显式传 --page-all 才会自动翻页并保留、合并 result.conversationMessagesList,同一会话跨页合并 messages。只传 --page-limit、--max-items 或 --page-delay 仍保持单页调用。自动翻页时 --page-limit 控制最多请求页数,--max-items 按消息数精确截断,--page-delay 控制页间等待毫秒数。`, Example: ` dws chat message list-mentions --start "2026-03-10T00:00:00+08:00" --end "2026-03-11T00:00:00+08:00" --limit 50 --cursor 0 dws chat message list-mentions --start "2026-04-01T00:00:00+08:00" --end "2026-04-14T00:00:00+08:00" --limit 20 --cursor 0 dws chat message list-mentions --group --start "2026-03-10T00:00:00+08:00" --end "2026-03-11T00:00:00+08:00" --limit 50 --cursor 0 @@ -3770,7 +3775,7 @@ func newChatCommand() *cobra.Command { chatMessageSearchCmd := &cobra.Command{ Use: "search", Short: "按关键词搜索消息", - Long: `在当前用户的会话中按关键词搜索消息。输出顶层 messages,稳定字段为 messageId 和 text;兼容保留 openMessageId、content 和原始 result。--query 指定搜索关键词(必填)。可选 --group 限定搜索某个会话,不传则搜索所有会话。显式指定会话时,CLI 会先验证 CID,再扫描全局搜索流并在本地精确过滤,避免下层忽略非法 CID 或群聊 CID;默认最多扫描 40 页并返回至 --limit 条范围内消息。时间参数 --start/--end(ISO-8601)限定搜索时间范围。分页参数 --limit(默认 100)和 --cursor(默认 "0")始终传递;hasMore=true 时用返回的 nextCursor 作为下次 --cursor 继续翻页。未指定会话时默认只读取单页;只有显式传 --page-all 才会自动翻页并保留、合并 result.conversationMessagesList,同一会话跨页合并 messages。只传 --page-limit、--max-items 或 --page-delay 仍保持默认行为。自动翻页时 --page-limit 控制最多请求页数,--max-items 按消息数精确截断,--page-delay 控制页间等待毫秒数。`, + Long: `在当前用户的会话中按关键词搜索消息。输出顶层 messages,稳定字段为 messageId 和 text;兼容保留 openMessageId、content 和原始 result。--query 指定搜索关键词(必填)。可选 --group 限定搜索某个会话,不传则搜索所有会话。显式指定会话时,CLI 会先验证 CID,再扫描全局搜索流并在本地精确过滤,避免下层忽略非法 CID 或群聊 CID;默认最多扫描 40 页并返回至 --limit 条范围内消息。时间参数 --start/--end(ISO-8601)可选,不传时默认最近 7 天到当前时间。分页参数 --limit(默认 100)和 --cursor(默认 "0")始终传递;hasMore=true 时用返回的 nextCursor 作为下次 --cursor 继续翻页。未指定会话时默认只读取单页;只有显式传 --page-all 才会自动翻页并保留、合并 result.conversationMessagesList,同一会话跨页合并 messages。只传 --page-limit、--max-items 或 --page-delay 仍保持默认行为。自动翻页时 --page-limit 控制最多请求页数,--max-items 按消息数精确截断,--page-delay 控制页间等待毫秒数。`, Example: ` dws chat message search --query "changefree" --start "2026-04-01T00:00:00+08:00" --end "2026-04-15T00:00:00+08:00" --limit 50 --cursor 0 dws chat message search --query "codereview" --group --start "2026-04-01T00:00:00+08:00" --end "2026-04-15T00:00:00+08:00" --limit 100 --cursor 0 dws chat message search --query "链接" --start "2026-04-15T00:00:00+08:00" --end "2026-04-16T00:00:00+08:00" --limit 100 --cursor @@ -4328,9 +4333,9 @@ chat message edit 或 chat message recall 的 --msg-id 和 --conversation-id。 chatMessageListCmd.Flags().String("group", "", "群聊 openconversation_id(群聊时必填)") chatMessageListCmd.Flags().String("user", "", "单聊用户 userId(单聊时与 --open-dingtalk-id 二选一)") chatMessageListCmd.Flags().String("open-dingtalk-id", "", "单聊用户 openDingTalkId(单聊时与 --user 二选一,适用于无法获取 userId 的场景)") - chatMessageListCmd.Flags().String("time", "", "开始时间,格式: yyyy-MM-dd HH:mm:ss (必填)") - chatMessageListCmd.Flags().String("direction", "", "时间方向: newer=从给定时间往现在拉,older=从给定时间往以前拉(推荐)") - chatMessageListCmd.Flags().String("forward", "true", "true 等价 --direction newer,false 等价 --direction older") + chatMessageListCmd.Flags().String("time", "", "开始时间,格式: yyyy-MM-dd HH:mm:ss(可选,默认当前时间)") + chatMessageListCmd.Flags().String("direction", "", "时间方向: newer=从给定时间往现在拉,older=从给定时间往以前拉(未传 --time 时默认 older)") + chatMessageListCmd.Flags().String("forward", "true", "true 等价 --direction newer,false 等价 --direction older(未传 --time 时默认 false)") _ = chatMessageListCmd.Flags().MarkHidden("forward") chatMessageListCmd.Flags().Int("limit", 0, "返回数量,不传则不限制") chatMessageListCmd.Flags().Int("size", 0, "--limit 的旧版别名") @@ -4444,10 +4449,8 @@ chat message edit 或 chat message recall 的 --msg-id 和 --conversation-id。 chatMessageListTopicRepliesCmd.Flags().String("forward", "false", "true 等价 --direction newer,false 等价 --direction older(默认 false)") _ = chatMessageListTopicRepliesCmd.Flags().MarkHidden("forward") - chatMessageListAllCmd.Flags().String("start", "", "起始时间,格式: yyyy-MM-dd HH:mm:ss (必填)") - _ = chatMessageListAllCmd.MarkFlagRequired("start") - chatMessageListAllCmd.Flags().String("end", "", "结束时间,格式: yyyy-MM-dd HH:mm:ss (必填)") - _ = chatMessageListAllCmd.MarkFlagRequired("end") + chatMessageListAllCmd.Flags().String("start", "", "起始时间,格式: yyyy-MM-dd HH:mm:ss(可选,默认当前时间前 1 天)") + chatMessageListAllCmd.Flags().String("end", "", "结束时间,格式: yyyy-MM-dd HH:mm:ss(可选,默认当前时间)") chatMessageListAllCmd.Flags().Int("limit", 50, "每页返回数量(默认 50)") chatMessageListAllCmd.Flags().Int("size", 0, "--limit 的旧版别名") _ = chatMessageListAllCmd.Flags().MarkHidden("size") @@ -4461,9 +4464,8 @@ chat message edit 或 chat message recall 的 --msg-id 和 --conversation-id。 chatMessageListBySenderCmd.Flags().String("sender-open-dingtalk-id", "", "发送者 openDingTalkId(与 --sender-user-id 二选一,适用于无法获取 userId 的场景)") chatMessageListBySenderCmd.Flags().String("user", "", "") _ = chatMessageListBySenderCmd.Flags().MarkHidden("user") - chatMessageListBySenderCmd.Flags().String("start", "", "开始时间,ISO-8601 格式 (必填)") - _ = chatMessageListBySenderCmd.MarkFlagRequired("start") - chatMessageListBySenderCmd.Flags().String("end", "", "结束时间,ISO-8601 格式 (必填)") + chatMessageListBySenderCmd.Flags().String("start", "", "开始时间,ISO-8601 格式(可选,默认当前时间前 7 天)") + chatMessageListBySenderCmd.Flags().String("end", "", "结束时间,ISO-8601 格式(可选,默认当前时间)") chatMessageListBySenderCmd.Flags().Int("limit", 50, "每页返回数量(默认 50)") chatMessageListBySenderCmd.Flags().Int("size", 0, "--limit 的旧版别名") _ = chatMessageListBySenderCmd.Flags().MarkHidden("size") @@ -4472,10 +4474,8 @@ chat message edit 或 chat message recall 的 --msg-id 和 --conversation-id。 // list-mentions flags chatMessageListMentionsCmd.Flags().String("group", "", "群聊 openconversation_id(可选,不传则查全部)") - chatMessageListMentionsCmd.Flags().String("start", "", "开始时间,ISO-8601 格式 (必填)") - _ = chatMessageListMentionsCmd.MarkFlagRequired("start") - chatMessageListMentionsCmd.Flags().String("end", "", "结束时间,ISO-8601 格式 (必填)") - _ = chatMessageListMentionsCmd.MarkFlagRequired("end") + chatMessageListMentionsCmd.Flags().String("start", "", "开始时间,ISO-8601 格式(可选,默认当前时间前 7 天)") + chatMessageListMentionsCmd.Flags().String("end", "", "结束时间,ISO-8601 格式(可选,默认当前时间)") chatMessageListMentionsCmd.Flags().Int("limit", 50, "每页返回数量(默认 50)") chatMessageListMentionsCmd.Flags().Int("size", 0, "--limit 的旧版别名") _ = chatMessageListMentionsCmd.Flags().MarkHidden("size") @@ -4500,10 +4500,8 @@ chat message edit 或 chat message recall 的 --msg-id 和 --conversation-id。 chatMessageSearchCmd.Flags().String("keyword", "", "--query 的别名") _ = chatMessageSearchCmd.Flags().MarkHidden("keyword") chatMessageSearchCmd.Flags().String("group", "", "群聊 openconversation_id(可选,不传则搜索所有会话)") - chatMessageSearchCmd.Flags().String("start", "", "开始时间,ISO-8601 格式 (必填)") - _ = chatMessageSearchCmd.MarkFlagRequired("start") - chatMessageSearchCmd.Flags().String("end", "", "结束时间,ISO-8601 格式 (必填)") - _ = chatMessageSearchCmd.MarkFlagRequired("end") + chatMessageSearchCmd.Flags().String("start", "", "开始时间,ISO-8601 格式(可选,默认当前时间前 7 天)") + chatMessageSearchCmd.Flags().String("end", "", "结束时间,ISO-8601 格式(可选,默认当前时间)") chatMessageSearchCmd.Flags().Int("limit", 100, "每页返回数量(默认 100)") chatMessageSearchCmd.Flags().Int("size", 0, "--limit 的旧版别名") _ = chatMessageSearchCmd.Flags().MarkHidden("size") @@ -5913,6 +5911,9 @@ flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成 Short: "下载消息中的资源(图片/视频/语音等)到本地", Long: `下载聊天消息中的图片、视频、语音等资源到本地文件。 +本命令只支持聊天消息 mediaId 下载,不支持钉盘 fileId。 +如需用 fileId 下载,请使用钉盘/drive 下载命令。 + 流程: 1. 根据 resourceType + resource-id + message-id + open-conversation-id 向服务端获取下载 URL 2. HTTP GET 下载文件到本地 @@ -5923,6 +5924,7 @@ flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成 dws chat message download-media --type mediaId --resource-id --message-id --open-conversation-id --output ./downloads/ dws chat message download-media --type mediaId --resource-id --message-id --open-conversation-id --output ./photo.jpg # resource-id: 从 dws chat message list 返回的消息内容中获取 mediaId + # 不支持钉盘 fileId;fileId 下载请使用钉盘/drive 下载命令 # message-id: 从 dws chat message list 返回的 openMessageId # open-conversation-id: 从 dws chat search 获取 openConversationId`, PreRunE: func(cmd *cobra.Command, args []string) error { @@ -6052,16 +6054,16 @@ flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成 Selection: contract.SelectionSpec{ AgentSummary: "下载消息中的媒体资源到本地", UseWhen: []string{"已知消息、会话和资源 ID,需要保存媒体文件时"}, - AvoidWhen: []string{"只查看文本消息内容时使用对应消息查询命令"}, + AvoidWhen: []string{"只查看文本消息内容时使用对应消息查询命令", "资源是钉盘 fileId 时使用钉盘/drive 下载命令"}, Examples: []string{"dws chat message download-media --type mediaId --resource-id --message-id --open-conversation-id --output ."}, }, }, }) // download-media flags - chatMessageDownloadMediaCmd.Flags().String("type", "", "资源类型: mediaId (必填)") + chatMessageDownloadMediaCmd.Flags().String("type", "", "资源类型: mediaId(必填;仅支持聊天消息 mediaId,不支持钉盘 fileId)") _ = chatMessageDownloadMediaCmd.MarkFlagRequired("type") - chatMessageDownloadMediaCmd.Flags().String("resource-id", "", "资源 ID,mediaId 类型时为消息中的 mediaId 值 (必填)") + chatMessageDownloadMediaCmd.Flags().String("resource-id", "", "资源 ID,mediaId 类型时为消息中的 mediaId 值(必填;不是钉盘 fileId)") _ = chatMessageDownloadMediaCmd.MarkFlagRequired("resource-id") chatMessageDownloadMediaCmd.Flags().String("open-conversation-id", "", "会话 openConversationId (必填)") _ = chatMessageDownloadMediaCmd.MarkFlagRequired("open-conversation-id") diff --git a/internal/helpers/chat_message_help_test.go b/internal/helpers/chat_message_help_test.go index fcf1548c..6941e2e8 100644 --- a/internal/helpers/chat_message_help_test.go +++ b/internal/helpers/chat_message_help_test.go @@ -90,3 +90,96 @@ func TestCrossPlatformCoverageChatMessageHelpDocumentsPostSendIDChain(t *testing }) } } + +func TestCrossPlatformCoverageChatMessageHelpDocumentsOptionalTimeDefaults(t *testing.T) { + tests := []struct { + name string + args []string + contains []string + absent []string + }{ + { + name: "list", + args: []string{"message", "list", "--help"}, + contains: []string{ + "--time 可选,不传时默认当前时间并向旧消息拉取", + "默认当前时间", + "未传 --time 时默认 older", + }, + absent: []string{"开始时间,格式: yyyy-MM-dd HH:mm:ss (必填)"}, + }, + { + name: "search", + args: []string{"message", "search", "--help"}, + contains: []string{ + "可选,不传时默认最近 7 天到当前时间", + "默认当前时间前 7 天", + "默认当前时间", + }, + absent: []string{"开始时间,ISO-8601 格式 (必填)", "结束时间,ISO-8601 格式 (必填)"}, + }, + { + name: "list-by-sender", + args: []string{"message", "list-by-sender", "--help"}, + contains: []string{ + "--start 和 --end 可选,不传时默认最近 7 天到当前时间", + "默认当前时间前 7 天", + "默认当前时间", + }, + absent: []string{"开始时间,ISO-8601 格式 (必填)", "结束时间,ISO-8601 格式 (必填)"}, + }, + { + name: "list-mentions", + args: []string{"message", "list-mentions", "--help"}, + contains: []string{ + "--start 和 --end 可选,不传时默认最近 7 天到当前时间", + "默认当前时间前 7 天", + "默认当前时间", + }, + absent: []string{"开始时间,ISO-8601 格式 (必填)", "结束时间,ISO-8601 格式 (必填)"}, + }, + { + name: "list-all", + args: []string{"message", "list-all", "--help"}, + contains: []string{ + "--start 和 --end 可选,不传时默认最近 1 天到当前时间", + "默认当前时间前 1 天", + "默认当前时间", + }, + absent: []string{"起始时间,格式: yyyy-MM-dd HH:mm:ss (必填)", "结束时间,格式: yyyy-MM-dd HH:mm:ss (必填)"}, + }, + { + name: "download-media", + args: []string{"message", "download-media", "--help"}, + contains: []string{ + "只支持聊天消息 mediaId 下载,不支持钉盘 fileId", + "fileId 下载请使用钉盘/drive 下载命令", + "仅支持聊天消息 mediaId,不支持钉盘 fileId", + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + cmd := newChatCommand() + var output bytes.Buffer + cmd.SetOut(&output) + cmd.SetErr(&output) + cmd.SetArgs(test.args) + if err := cmd.Execute(); err != nil { + t.Fatalf("dws chat %s: %v\n%s", strings.Join(test.args, " "), err, output.String()) + } + help := output.String() + for _, want := range test.contains { + if !strings.Contains(help, want) { + t.Errorf("chat message %s help missing %q:\n%s", test.name, want, help) + } + } + for _, unwanted := range test.absent { + if strings.Contains(help, unwanted) { + t.Errorf("chat message %s help still contains %q:\n%s", test.name, unwanted, help) + } + } + }) + } +} diff --git a/internal/helpers/chat_message_pagination_test.go b/internal/helpers/chat_message_pagination_test.go index 21bac2c7..2b6c28ab 100644 --- a/internal/helpers/chat_message_pagination_test.go +++ b/internal/helpers/chat_message_pagination_test.go @@ -148,6 +148,90 @@ func TestChatMessagePaginationDefaultSinglePageUnchanged(t *testing.T) { } } +func TestChatMessagePaginationUsesDefaultTimeWindows(t *testing.T) { + tests := []struct { + name string + args []string + wantTool string + wantLookback time.Duration + }{ + { + name: "list-all defaults to one day", + args: []string{"message", "list-all"}, + wantTool: "search_messages_by_time_range", + wantLookback: 24 * time.Hour, + }, + { + name: "list-by-sender defaults to seven days", + args: []string{"message", "list-by-sender", "--sender-user-id", "u1"}, + wantTool: "search_messages_by_sender", + wantLookback: 7 * 24 * time.Hour, + }, + { + name: "list-mentions defaults to seven days", + args: []string{"message", "list-mentions"}, + wantTool: "search_at_me_message", + wantLookback: 7 * 24 * time.Hour, + }, + { + name: "search defaults to seven days", + args: []string{"message", "search", "--query", "发布"}, + wantTool: "search_messages_by_keyword", + wantLookback: 7 * 24 * time.Hour, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + caller := &chatMessagePaginationCaller{} + before := time.Now() + _, err := executeChatMessagePaginationCommand(t, caller, tt.args...) + after := time.Now() + if err != nil { + t.Fatal(err) + } + if len(caller.calls) != 1 { + t.Fatalf("calls = %#v, want one call", caller.calls) + } + got := caller.calls[0] + if got.tool != tt.wantTool { + t.Fatalf("tool = %q, want %q", got.tool, tt.wantTool) + } + startMs := numericArgAsInt64(t, got.args["startTime"]) + endMs := numericArgAsInt64(t, got.args["endTime"]) + if endMs < before.UnixMilli() || endMs > after.Add(time.Second).UnixMilli() { + t.Fatalf("endTime = %d, want between %d and %d", endMs, before.UnixMilli(), after.Add(time.Second).UnixMilli()) + } + wantStartMin := before.Add(-tt.wantLookback).UnixMilli() + wantStartMax := after.Add(-tt.wantLookback).Add(time.Second).UnixMilli() + if startMs < wantStartMin || startMs > wantStartMax { + t.Fatalf("startTime = %d, want between %d and %d", startMs, wantStartMin, wantStartMax) + } + }) + } +} + +func numericArgAsInt64(t *testing.T, value any) int64 { + t.Helper() + switch v := value.(type) { + case int64: + return v + case int: + return int64(v) + case float64: + return int64(v) + case string: + parsed, err := parseISOTimeToMillis("time", v) + if err != nil { + t.Fatalf("parse time arg %q: %v", v, err) + } + return parsed + default: + t.Fatalf("unsupported numeric arg type %T (%#v)", value, value) + return 0 + } +} + func TestChatMessagePaginationPageAllAggregatesSevenCommands(t *testing.T) { tests := []struct { name string diff --git a/internal/helpers/im_read_result_contract_test.go b/internal/helpers/im_read_result_contract_test.go index 3cff91cc..d2df5bc2 100644 --- a/internal/helpers/im_read_result_contract_test.go +++ b/internal/helpers/im_read_result_contract_test.go @@ -22,6 +22,7 @@ import ( "os" "reflect" "testing" + "time" "github.com/spf13/cobra" @@ -39,11 +40,16 @@ type imReadResultCaller struct { 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, _ map[string]any) (*edition.ToolResult, error) { +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 } @@ -203,6 +209,36 @@ func TestCrossPlatformCoverageChatMessageListProjectsStableFieldsAndPreservesLeg } } +func TestCrossPlatformCoverageChatMessageListDefaultsTimeAndOlderDirection(t *testing.T) { + payload := `{"result":{"messages":[],"hasMore":false}}` + caller := &imReadResultCaller{responses: map[string]string{"list_conversation_message_v2": payload}} + + before := time.Now() + _, err := executeIMReadCommand(t, caller, []string{"dws", "chat"}, newChatCommand, + "message", "list", "--group", "cid-1", "--limit", "50") + after := time.Now() + 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, time.Local) + 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": { diff --git a/test/fixtures/cli-interface-baseline.txt b/test/fixtures/cli-interface-baseline.txt index fab13963..db802415 100644 --- a/test/fixtures/cli-interface-baseline.txt +++ b/test/fixtures/cli-interface-baseline.txt @@ -2692,7 +2692,7 @@ [chat.message.list-all] runnable: true hidden: false - flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --cursor:string|required=false|hidden=false|no-opt=""|scope=local, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --end:string|required=true|hidden=false|no-opt=""|scope=local, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --limit:int|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --size:int|required=false|hidden=true|no-opt=""|scope=local, --start:string|required=true|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --cursor:string|required=false|hidden=false|no-opt=""|scope=local, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --end:string|required=false|hidden=false|no-opt=""|scope=local, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --limit:int|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --size:int|required=false|hidden=true|no-opt=""|scope=local, --start:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited [chat.message.list-by-ids] runnable: true @@ -2702,7 +2702,7 @@ [chat.message.list-by-sender] runnable: true hidden: false - flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --cursor:string|required=false|hidden=false|no-opt=""|scope=local, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --end:string|required=false|hidden=false|no-opt=""|scope=local, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --limit:int|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --sender:string|required=false|hidden=true|no-opt=""|scope=local, --sender-open-dingtalk-id:string|required=false|hidden=false|no-opt=""|scope=local, --sender-user-id:string|required=false|hidden=false|no-opt=""|scope=local, --size:int|required=false|hidden=true|no-opt=""|scope=local, --start:string|required=true|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, --user:string|required=false|hidden=true|no-opt=""|scope=local, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + flags: --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --cursor:string|required=false|hidden=false|no-opt=""|scope=local, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --end:string|required=false|hidden=false|no-opt=""|scope=local, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --limit:int|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --sender:string|required=false|hidden=true|no-opt=""|scope=local, --sender-open-dingtalk-id:string|required=false|hidden=false|no-opt=""|scope=local, --sender-user-id:string|required=false|hidden=false|no-opt=""|scope=local, --size:int|required=false|hidden=true|no-opt=""|scope=local, --start:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, --user:string|required=false|hidden=true|no-opt=""|scope=local, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited [chat.message.list-emotion-replies] runnable: true @@ -2722,7 +2722,7 @@ [chat.message.list-mentions] runnable: true hidden: false - flags: --chat:string|required=false|hidden=true|no-opt=""|scope=local, --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --conversation-id:string|required=false|hidden=true|no-opt=""|scope=local, --cursor:string|required=false|hidden=false|no-opt=""|scope=local, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --end:string|required=true|hidden=false|no-opt=""|scope=local, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, --group:string|required=false|hidden=false|no-opt=""|scope=local, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --id:string|required=false|hidden=true|no-opt=""|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --limit:int|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --size:int|required=false|hidden=true|no-opt=""|scope=local, --start:string|required=true|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + flags: --chat:string|required=false|hidden=true|no-opt=""|scope=local, --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --conversation-id:string|required=false|hidden=true|no-opt=""|scope=local, --cursor:string|required=false|hidden=false|no-opt=""|scope=local, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --end:string|required=false|hidden=false|no-opt=""|scope=local, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, --group:string|required=false|hidden=false|no-opt=""|scope=local, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --id:string|required=false|hidden=true|no-opt=""|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --limit:int|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --size:int|required=false|hidden=true|no-opt=""|scope=local, --start:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited [chat.message.list-pin-msg] runnable: true @@ -2782,7 +2782,7 @@ [chat.message.search] runnable: true hidden: false - flags: --chat:string|required=false|hidden=true|no-opt=""|scope=local, --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --conversation-id:string|required=false|hidden=true|no-opt=""|scope=local, --cursor:string|required=false|hidden=false|no-opt=""|scope=local, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --end:string|required=true|hidden=false|no-opt=""|scope=local, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, --group:string|required=false|hidden=false|no-opt=""|scope=local, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --id:string|required=false|hidden=true|no-opt=""|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --keyword:string|required=false|hidden=true|no-opt=""|scope=local, --limit:int|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --query:string|required=false|hidden=false|no-opt=""|scope=local, --size:int|required=false|hidden=true|no-opt=""|scope=local, --start:string|required=true|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited + flags: --chat:string|required=false|hidden=true|no-opt=""|scope=local, --client-id:string|required=false|hidden=false|no-opt=""|scope=inherited, --client-secret:string|required=false|hidden=false|no-opt=""|scope=inherited, --conversation-id:string|required=false|hidden=true|no-opt=""|scope=local, --cursor:string|required=false|hidden=false|no-opt=""|scope=local, --debug:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --dry-run:bool|required=false|hidden=false|no-opt="true"|scope=inherited, --end:string|required=false|hidden=false|no-opt=""|scope=local, --fields:string|required=false|hidden=false|no-opt=""|scope=inherited, -f/--format:string|required=false|hidden=false|no-opt=""|scope=inherited, --group:string|required=false|hidden=false|no-opt=""|scope=local, -h/--help:bool|required=false|hidden=false|no-opt="true"|scope=local, --id:string|required=false|hidden=true|no-opt=""|scope=local, --jq:string|required=false|hidden=false|no-opt=""|scope=inherited, --keyword:string|required=false|hidden=true|no-opt=""|scope=local, --limit:int|required=false|hidden=false|no-opt=""|scope=local, --mock:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -o/--output:string|required=false|hidden=true|no-opt=""|scope=inherited, --profile:string|required=false|hidden=false|no-opt=""|scope=inherited, --query:string|required=false|hidden=false|no-opt=""|scope=local, --size:int|required=false|hidden=true|no-opt=""|scope=local, --start:string|required=false|hidden=false|no-opt=""|scope=local, --timeout:int|required=false|hidden=false|no-opt=""|scope=inherited, --token:string|required=false|hidden=true|no-opt=""|scope=inherited, -v/--verbose:bool|required=false|hidden=false|no-opt="true"|scope=inherited, -y/--yes:bool|required=false|hidden=false|no-opt="true"|scope=inherited [chat.message.search-advanced] runnable: true From 9c14d9a6e15d5b1a0e090f9fbebef24fbf40f145 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=95=BF=E7=9C=9F?= Date: Wed, 12 Aug 2026 16:42:27 +0800 Subject: [PATCH 02/16] fix(chat): repair message time defaults checks --- internal/helpers/chat.go | 1 - internal/helpers/chat_message_pagination_test.go | 8 +++++--- internal/helpers/im_read_result_contract_test.go | 1 + 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/internal/helpers/chat.go b/internal/helpers/chat.go index 3cc7a516..83bc0567 100644 --- a/internal/helpers/chat.go +++ b/internal/helpers/chat.go @@ -2700,7 +2700,6 @@ func newChatCommand() *cobra.Command { AvoidWhen: []string{"跨全部会话按时间查询时使用 chat message list-all"}, Examples: []string{ "dws chat message list --group --limit 50", - "dws chat message list --group --time \"2026-07-01 00:00:00\" --limit 50", "dws chat message list --group --time \"2026-07-01 00:00:00\" --limit 50 --jq '.messages[] | {messageId, text}'", }, }, diff --git a/internal/helpers/chat_message_pagination_test.go b/internal/helpers/chat_message_pagination_test.go index 2b6c28ab..2706b621 100644 --- a/internal/helpers/chat_message_pagination_test.go +++ b/internal/helpers/chat_message_pagination_test.go @@ -199,10 +199,12 @@ func TestChatMessagePaginationUsesDefaultTimeWindows(t *testing.T) { } startMs := numericArgAsInt64(t, got.args["startTime"]) endMs := numericArgAsInt64(t, got.args["endTime"]) - if endMs < before.UnixMilli() || endMs > after.Add(time.Second).UnixMilli() { - t.Fatalf("endTime = %d, want between %d and %d", endMs, before.UnixMilli(), after.Add(time.Second).UnixMilli()) + wantEndMin := before.Truncate(time.Second).UnixMilli() + wantEndMax := after.Add(time.Second).UnixMilli() + if endMs < wantEndMin || endMs > wantEndMax { + t.Fatalf("endTime = %d, want between %d and %d", endMs, wantEndMin, wantEndMax) } - wantStartMin := before.Add(-tt.wantLookback).UnixMilli() + wantStartMin := before.Add(-tt.wantLookback).Truncate(time.Second).UnixMilli() wantStartMax := after.Add(-tt.wantLookback).Add(time.Second).UnixMilli() if startMs < wantStartMin || startMs > wantStartMax { t.Fatalf("startTime = %d, want between %d and %d", startMs, wantStartMin, wantStartMax) diff --git a/internal/helpers/im_read_result_contract_test.go b/internal/helpers/im_read_result_contract_test.go index d2df5bc2..0d983925 100644 --- a/internal/helpers/im_read_result_contract_test.go +++ b/internal/helpers/im_read_result_contract_test.go @@ -21,6 +21,7 @@ import ( "io" "os" "reflect" + "strings" "testing" "time" From d40a22aeb02fab1cd6403c4888c44289ea269a35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=95=BF=E7=9C=9F?= Date: Wed, 12 Aug 2026 20:17:10 +0800 Subject: [PATCH 03/16] fix(chat): default start from explicit message end --- internal/helpers/chat.go | 36 +++-- .../helpers/chat_message_pagination_test.go | 128 ++++++++++++++++++ 2 files changed, 155 insertions(+), 9 deletions(-) diff --git a/internal/helpers/chat.go b/internal/helpers/chat.go index 83bc0567..a1fef36d 100644 --- a/internal/helpers/chat.go +++ b/internal/helpers/chat.go @@ -467,7 +467,10 @@ func validateNativeSearchConversationScope(conversationIDs []string) error { } func chatMessageListAllArgs(cmd *cobra.Command) (map[string]any, error) { - startRaw, endRaw := defaultChatMessageTimeRange(cmd, 24*time.Hour) + startRaw, endRaw, err := defaultChatMessageTimeRange(cmd, 24*time.Hour) + if err != nil { + return nil, err + } cursor, _ := cmd.Flags().GetString("cursor") return map[string]any{ "startTime": startRaw, @@ -486,7 +489,10 @@ func chatMessageListBySenderArgs(cmd *cobra.Command) (map[string]any, error) { if senderUserID == "" && senderOpenDingTalkID == "" { return nil, fmt.Errorf("--sender-user-id or --sender-open-dingtalk-id is required") } - startRaw, endRaw := defaultChatMessageTimeRange(cmd, 7*24*time.Hour) + startRaw, endRaw, err := defaultChatMessageTimeRange(cmd, 7*24*time.Hour) + if err != nil { + return nil, err + } startMs, err := parseISOTimeToMillis("start", startRaw) if err != nil { return nil, err @@ -514,7 +520,10 @@ func chatMessageListBySenderArgs(cmd *cobra.Command) (map[string]any, error) { } func chatMessageListMentionsArgs(cmd *cobra.Command) (map[string]any, error) { - startRaw, endRaw := defaultChatMessageTimeRange(cmd, 7*24*time.Hour) + startRaw, endRaw, err := defaultChatMessageTimeRange(cmd, 7*24*time.Hour) + if err != nil { + return nil, err + } startMs, err := parseISOTimeToMillis("start", startRaw) if err != nil { return nil, err @@ -554,7 +563,10 @@ func chatMessageSearchArgs(cmd *cobra.Command) (map[string]any, error) { if err := validateRequiredFlagWithAliases(cmd, "query", "keyword"); err != nil { return nil, err } - startRaw, endRaw := defaultChatMessageTimeRange(cmd, 7*24*time.Hour) + startRaw, endRaw, err := defaultChatMessageTimeRange(cmd, 7*24*time.Hour) + if err != nil { + return nil, err + } startMs, err := parseISOTimeToMillis("start", startRaw) if err != nil { return nil, err @@ -580,17 +592,23 @@ func chatMessageSearchArgs(cmd *cobra.Command) (map[string]any, error) { return toolArgs, nil } -func defaultChatMessageTimeRange(cmd *cobra.Command, lookback time.Duration) (string, string) { +func defaultChatMessageTimeRange(cmd *cobra.Command, lookback time.Duration) (string, string, error) { startRaw := mustGetFlag(cmd, "start") endRaw := mustGetFlag(cmd, "end") - now := time.Now() + anchor := time.Now() if endRaw == "" { - endRaw = now.Format(time.RFC3339) + endRaw = anchor.Format(time.RFC3339) + } else { + endMs, err := parseISOTimeToMillis("end", endRaw) + if err != nil { + return "", "", err + } + anchor = time.UnixMilli(endMs) } if startRaw == "" { - startRaw = now.Add(-lookback).Format(time.RFC3339) + startRaw = anchor.Add(-lookback).Format(time.RFC3339) } - return startRaw, endRaw + return startRaw, endRaw, nil } func chatMessageSearchAdvancedArgs(cmd *cobra.Command) (map[string]any, error) { diff --git a/internal/helpers/chat_message_pagination_test.go b/internal/helpers/chat_message_pagination_test.go index 2706b621..8911b5e1 100644 --- a/internal/helpers/chat_message_pagination_test.go +++ b/internal/helpers/chat_message_pagination_test.go @@ -213,6 +213,134 @@ func TestChatMessagePaginationUsesDefaultTimeWindows(t *testing.T) { } } +func TestChatMessagePaginationDefaultsStartFromExplicitEnd(t *testing.T) { + endRaw := "2026-01-01T00:00:00+08:00" + endMs, err := parseISOTimeToMillis("end", endRaw) + if err != nil { + t.Fatal(err) + } + tests := []struct { + name string + args []string + wantTool string + wantLookback time.Duration + }{ + { + name: "list-all defaults start one day before explicit end", + args: []string{"message", "list-all", "--end", endRaw}, + wantTool: "search_messages_by_time_range", + wantLookback: 24 * time.Hour, + }, + { + name: "list-by-sender defaults start seven days before explicit end", + args: []string{"message", "list-by-sender", "--sender-user-id", "u1", "--end", endRaw}, + wantTool: "search_messages_by_sender", + wantLookback: 7 * 24 * time.Hour, + }, + { + name: "list-mentions defaults start seven days before explicit end", + args: []string{"message", "list-mentions", "--end", endRaw}, + wantTool: "search_at_me_message", + wantLookback: 7 * 24 * time.Hour, + }, + { + name: "search defaults start seven days before explicit end", + args: []string{"message", "search", "--query", "发布", "--end", endRaw}, + wantTool: "search_messages_by_keyword", + wantLookback: 7 * 24 * time.Hour, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + caller := &chatMessagePaginationCaller{} + _, err := executeChatMessagePaginationCommand(t, caller, tt.args...) + if err != nil { + t.Fatal(err) + } + if len(caller.calls) != 1 { + t.Fatalf("calls = %#v, want one call", caller.calls) + } + got := caller.calls[0] + if got.tool != tt.wantTool { + t.Fatalf("tool = %q, want %q", got.tool, tt.wantTool) + } + startMs := numericArgAsInt64(t, got.args["startTime"]) + gotEndMs := numericArgAsInt64(t, got.args["endTime"]) + if gotEndMs != endMs { + t.Fatalf("endTime = %d, want %d", gotEndMs, endMs) + } + wantStartMs := time.UnixMilli(endMs).Add(-tt.wantLookback).UnixMilli() + if startMs != wantStartMs { + t.Fatalf("startTime = %d, want %d", startMs, wantStartMs) + } + }) + } +} + +func TestChatMessagePaginationDefaultsEndFromNowWhenOnlyStartProvided(t *testing.T) { + startRaw := "2026-01-01T00:00:00+08:00" + startMs, err := parseISOTimeToMillis("start", startRaw) + if err != nil { + t.Fatal(err) + } + tests := []struct { + name string + args []string + wantTool string + }{ + { + name: "list-all defaults end to now", + args: []string{"message", "list-all", "--start", startRaw}, + wantTool: "search_messages_by_time_range", + }, + { + name: "list-by-sender defaults end to now", + args: []string{"message", "list-by-sender", "--sender-user-id", "u1", "--start", startRaw}, + wantTool: "search_messages_by_sender", + }, + { + name: "list-mentions defaults end to now", + args: []string{"message", "list-mentions", "--start", startRaw}, + wantTool: "search_at_me_message", + }, + { + name: "search defaults end to now", + args: []string{"message", "search", "--query", "发布", "--start", startRaw}, + wantTool: "search_messages_by_keyword", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + caller := &chatMessagePaginationCaller{} + before := time.Now() + _, err := executeChatMessagePaginationCommand(t, caller, tt.args...) + after := time.Now() + if err != nil { + t.Fatal(err) + } + if len(caller.calls) != 1 { + t.Fatalf("calls = %#v, want one call", caller.calls) + } + got := caller.calls[0] + if got.tool != tt.wantTool { + t.Fatalf("tool = %q, want %q", got.tool, tt.wantTool) + } + gotStartMs := numericArgAsInt64(t, got.args["startTime"]) + if gotStartMs != startMs { + t.Fatalf("startTime = %d, want %d", gotStartMs, startMs) + } + endMs := numericArgAsInt64(t, got.args["endTime"]) + wantEndMin := before.Truncate(time.Second).UnixMilli() + wantEndMax := after.Add(time.Second).UnixMilli() + if endMs < wantEndMin || endMs > wantEndMax { + t.Fatalf("endTime = %d, want between %d and %d", endMs, wantEndMin, wantEndMax) + } + }) + } +} + func numericArgAsInt64(t *testing.T, value any) int64 { t.Helper() switch v := value.(type) { From 30f3273a17b6ec5d968e8c530d930f0eb4449d45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=95=BF=E7=9C=9F?= Date: Wed, 12 Aug 2026 20:43:56 +0800 Subject: [PATCH 04/16] fix(chat): validate message list-all time range --- .changes/973-chat-message-time-defaults.md | 5 +++ internal/helpers/chat.go | 11 ++++++ .../helpers/chat_message_pagination_test.go | 36 +++++++++++++++++++ 3 files changed, 52 insertions(+) create mode 100644 .changes/973-chat-message-time-defaults.md diff --git a/.changes/973-chat-message-time-defaults.md b/.changes/973-chat-message-time-defaults.md new file mode 100644 index 00000000..1a6e5496 --- /dev/null +++ b/.changes/973-chat-message-time-defaults.md @@ -0,0 +1,5 @@ +--- +category: Fixed +--- + +- **Chat message time defaults** (#973) — validates `chat message list-all` time ranges locally and defaults omitted message-query time bounds without creating reversed windows. diff --git a/internal/helpers/chat.go b/internal/helpers/chat.go index a1fef36d..d9352996 100644 --- a/internal/helpers/chat.go +++ b/internal/helpers/chat.go @@ -471,6 +471,17 @@ func chatMessageListAllArgs(cmd *cobra.Command) (map[string]any, error) { if err != nil { return nil, err } + startMs, err := parseISOTimeToMillis("start", startRaw) + if err != nil { + return nil, err + } + endMs, err := parseISOTimeToMillis("end", endRaw) + if err != nil { + return nil, err + } + if err := validateTimeRange(startMs, endMs); err != nil { + return nil, err + } cursor, _ := cmd.Flags().GetString("cursor") return map[string]any{ "startTime": startRaw, diff --git a/internal/helpers/chat_message_pagination_test.go b/internal/helpers/chat_message_pagination_test.go index 8911b5e1..221bffa3 100644 --- a/internal/helpers/chat_message_pagination_test.go +++ b/internal/helpers/chat_message_pagination_test.go @@ -6,6 +6,7 @@ import ( "encoding/json" "io" "reflect" + "strings" "testing" "time" @@ -341,6 +342,41 @@ func TestChatMessagePaginationDefaultsEndFromNowWhenOnlyStartProvided(t *testing } } +func TestChatMessageListAllRejectsInvalidTimeRanges(t *testing.T) { + tests := []struct { + name string + args []string + wantErr string + }{ + { + name: "invalid start", + args: []string{"message", "list-all", "--start", "not-a-time", "--end", "2020-01-01T00:00:00+08:00"}, + wantErr: "cannot parse time for --start", + }, + { + name: "end before start", + args: []string{"message", "list-all", "--start", "2021-01-01T00:00:00+08:00", "--end", "2020-01-01T00:00:00+08:00"}, + wantErr: "--end must be after --start", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + caller := &chatMessagePaginationCaller{} + _, err := executeChatMessagePaginationCommand(t, caller, tt.args...) + if err == nil { + t.Fatal("command succeeded, want validation error") + } + if !strings.Contains(err.Error(), tt.wantErr) { + t.Fatalf("error = %v, want containing %q", err, tt.wantErr) + } + if len(caller.calls) != 0 { + t.Fatalf("calls = %#v, want no MCP call", caller.calls) + } + }) + } +} + func numericArgAsInt64(t *testing.T, value any) int64 { t.Helper() switch v := value.(type) { From 657f9ee3684a4a35a1dc5fb12d49829593f46306 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=95=BF=E7=9C=9F?= Date: Wed, 12 Aug 2026 22:29:22 +0800 Subject: [PATCH 05/16] ci(test): extend app race shard timeout --- .github/workflows/ci.yml | 4 ++-- test/scripts/changelog_pr_gate_test.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eace8f78..2815611e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -531,10 +531,10 @@ jobs: test -n "$package_output" mapfile -t packages <<< "$package_output" test "${#packages[@]}" -gt 0 - # cli/smoke own heavy NewRootCommand / Schema assembly under -race; give + # app/cli/smoke own heavy NewRootCommand / Schema assembly under -race; give # them a dedicated budget so remaining is not SIGTERM'd by OOM/timeout. timeout_budget=12m - if [ "$TEST_SHARD" = "cli" ] || [ "$TEST_SHARD" = "smoke" ]; then + if [ "$TEST_SHARD" = "app" ] || [ "$TEST_SHARD" = "cli" ] || [ "$TEST_SHARD" = "smoke" ]; then timeout_budget=15m fi go test -v -race -count=1 -timeout="$timeout_budget" "${packages[@]}" diff --git a/test/scripts/changelog_pr_gate_test.go b/test/scripts/changelog_pr_gate_test.go index bf05855b..0a24168d 100644 --- a/test/scripts/changelog_pr_gate_test.go +++ b/test/scripts/changelog_pr_gate_test.go @@ -1154,10 +1154,10 @@ func TestChangelogPRFastPathWorkflowContract(t *testing.T) { } raceJob := admission[raceStart:raceEnd] // Full race shards use a dynamic package-level timeout: default/floor 12m, - // with cli/smoke raised to 15m for NewRootCommand / Schema assembly under -race. + // with app/cli/smoke raised to 15m for NewRootCommand / Schema assembly under -race. for _, want := range []string{ "timeout_budget=12m", - `if [ "$TEST_SHARD" = "cli" ] || [ "$TEST_SHARD" = "smoke" ]; then`, + `if [ "$TEST_SHARD" = "app" ] || [ "$TEST_SHARD" = "cli" ] || [ "$TEST_SHARD" = "smoke" ]; then`, "timeout_budget=15m", `go test -v -race -count=1 -timeout="$timeout_budget" "${packages[@]}"`, "- smoke", From b92ac4db0fab5603b3b35c183e21bb9f87ffb548 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=95=BF=E7=9C=9F?= Date: Wed, 12 Aug 2026 23:16:33 +0800 Subject: [PATCH 06/16] fix(chat): preserve list-all time format --- internal/helpers/chat.go | 15 +++--- internal/helpers/chat_command_edges_test.go | 2 + .../helpers/chat_message_pagination_test.go | 52 ++++++++++++++++--- 3 files changed, 57 insertions(+), 12 deletions(-) diff --git a/internal/helpers/chat.go b/internal/helpers/chat.go index 464095b8..532a1584 100644 --- a/internal/helpers/chat.go +++ b/internal/helpers/chat.go @@ -475,17 +475,16 @@ func chatMessageListAllArgs(cmd *cobra.Command) (map[string]any, error) { if err != nil { return nil, err } - endMs, err := parseISOTimeToMillis("end", endRaw) - if err != nil { - return nil, err - } + endMs, _ := parseISOTimeToMillis("end", endRaw) if err := validateTimeRange(startMs, endMs); err != nil { return nil, err } + startTime := formatChatMessageListAllTime(startMs) + endTime := formatChatMessageListAllTime(endMs) cursor, _ := cmd.Flags().GetString("cursor") return map[string]any{ - "startTime": startRaw, - "endTime": endRaw, + "startTime": startTime, + "endTime": endTime, "limit": chatIntFlagOrFallback(cmd, "limit", "size"), "cursor": cursor, }, nil @@ -622,6 +621,10 @@ func defaultChatMessageTimeRange(cmd *cobra.Command, lookback time.Duration) (st return startRaw, endRaw, nil } +func formatChatMessageListAllTime(ms int64) string { + return time.UnixMilli(ms).In(shanghaiLocation()).Format("2006-01-02 15:04:05") +} + func chatMessageSearchAdvancedArgs(cmd *cobra.Command) (map[string]any, error) { toolArgs := map[string]any{} if v := flagOrFallback(cmd, "query", "keyword"); v != "" { diff --git a/internal/helpers/chat_command_edges_test.go b/internal/helpers/chat_command_edges_test.go index 6b12c044..cadc2207 100644 --- a/internal/helpers/chat_command_edges_test.go +++ b/internal/helpers/chat_command_edges_test.go @@ -168,6 +168,8 @@ func TestCrossPlatformCoverageChatCommandValidationAndSuccessEdges(t *testing.T) {"message", "list", "--user=D-user", "--time=2026-01-01", "--limit=1"}, {"message", "list", "--open-dingtalk-id=D-user", "--time=2026-01-01", "--direction=sideways"}, {"message", "list-direct", "--user=D-user", "--limit=1"}, + {"message", "list-all", "--start=2026-01-01 00:00:00", "--end=bad"}, + {"message", "list-all", "--start=2026-01-02 00:00:00", "--end=2026-01-01 00:00:00"}, {"message", "list-by-sender", "--sender-user-id=u1", "--start=bad"}, {"message", "list-by-sender", "--sender-user-id=u1", "--start=2026-01-01T00:00:00Z", "--end=bad"}, {"message", "list-by-sender", "--sender-user-id=u1", "--start=2026-01-02T00:00:00Z", "--end=2026-01-01T00:00:00Z"}, diff --git a/internal/helpers/chat_message_pagination_test.go b/internal/helpers/chat_message_pagination_test.go index 221bffa3..78eca26c 100644 --- a/internal/helpers/chat_message_pagination_test.go +++ b/internal/helpers/chat_message_pagination_test.go @@ -198,8 +198,8 @@ func TestChatMessagePaginationUsesDefaultTimeWindows(t *testing.T) { if got.tool != tt.wantTool { t.Fatalf("tool = %q, want %q", got.tool, tt.wantTool) } - startMs := numericArgAsInt64(t, got.args["startTime"]) - endMs := numericArgAsInt64(t, got.args["endTime"]) + startMs := chatMessageTimeArgAsMillis(t, got.args["startTime"], tt.wantTool) + endMs := chatMessageTimeArgAsMillis(t, got.args["endTime"], tt.wantTool) wantEndMin := before.Truncate(time.Second).UnixMilli() wantEndMax := after.Add(time.Second).UnixMilli() if endMs < wantEndMin || endMs > wantEndMax { @@ -266,8 +266,8 @@ func TestChatMessagePaginationDefaultsStartFromExplicitEnd(t *testing.T) { if got.tool != tt.wantTool { t.Fatalf("tool = %q, want %q", got.tool, tt.wantTool) } - startMs := numericArgAsInt64(t, got.args["startTime"]) - gotEndMs := numericArgAsInt64(t, got.args["endTime"]) + startMs := chatMessageTimeArgAsMillis(t, got.args["startTime"], tt.wantTool) + gotEndMs := chatMessageTimeArgAsMillis(t, got.args["endTime"], tt.wantTool) if gotEndMs != endMs { t.Fatalf("endTime = %d, want %d", gotEndMs, endMs) } @@ -275,6 +275,10 @@ func TestChatMessagePaginationDefaultsStartFromExplicitEnd(t *testing.T) { if startMs != wantStartMs { t.Fatalf("startTime = %d, want %d", startMs, wantStartMs) } + if tt.wantTool == "search_messages_by_time_range" { + assertStringArg(t, got.args["startTime"], "2025-12-31 00:00:00") + assertStringArg(t, got.args["endTime"], "2026-01-01 00:00:00") + } }) } } @@ -328,16 +332,20 @@ func TestChatMessagePaginationDefaultsEndFromNowWhenOnlyStartProvided(t *testing if got.tool != tt.wantTool { t.Fatalf("tool = %q, want %q", got.tool, tt.wantTool) } - gotStartMs := numericArgAsInt64(t, got.args["startTime"]) + gotStartMs := chatMessageTimeArgAsMillis(t, got.args["startTime"], tt.wantTool) if gotStartMs != startMs { t.Fatalf("startTime = %d, want %d", gotStartMs, startMs) } - endMs := numericArgAsInt64(t, got.args["endTime"]) + endMs := chatMessageTimeArgAsMillis(t, got.args["endTime"], tt.wantTool) wantEndMin := before.Truncate(time.Second).UnixMilli() wantEndMax := after.Add(time.Second).UnixMilli() if endMs < wantEndMin || endMs > wantEndMax { t.Fatalf("endTime = %d, want between %d and %d", endMs, wantEndMin, wantEndMax) } + if tt.wantTool == "search_messages_by_time_range" { + assertStringArg(t, got.args["startTime"], "2026-01-01 00:00:00") + parseChatMessageListAllTimeArg(t, got.args["endTime"]) + } }) } } @@ -398,6 +406,38 @@ func numericArgAsInt64(t *testing.T, value any) int64 { } } +func chatMessageTimeArgAsMillis(t *testing.T, value any, tool string) int64 { + t.Helper() + if tool == "search_messages_by_time_range" { + return parseChatMessageListAllTimeArg(t, value).UnixMilli() + } + return numericArgAsInt64(t, value) +} + +func parseChatMessageListAllTimeArg(t *testing.T, value any) time.Time { + t.Helper() + raw, ok := value.(string) + if !ok { + t.Fatalf("time arg = %#v, want yyyy-MM-dd HH:mm:ss string", value) + } + if strings.Contains(raw, "T") { + t.Fatalf("time arg = %q, want yyyy-MM-dd HH:mm:ss without RFC3339 separator", raw) + } + parsed, err := time.ParseInLocation("2006-01-02 15:04:05", raw, shanghaiLocation()) + if err != nil { + t.Fatalf("time arg = %q, parse err = %v", raw, err) + } + return parsed +} + +func assertStringArg(t *testing.T, value any, want string) { + t.Helper() + got, ok := value.(string) + if !ok || got != want { + t.Fatalf("arg = %#v, want %q", value, want) + } +} + func TestChatMessagePaginationPageAllAggregatesSevenCommands(t *testing.T) { tests := []struct { name string From 19a21b8f7e788034667cbac61c90d5b05aff516d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=95=BF=E7=9C=9F?= Date: Thu, 13 Aug 2026 10:54:51 +0800 Subject: [PATCH 07/16] fix(chat): avoid explicit zone in list-all formatting --- internal/helpers/chat.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/helpers/chat.go b/internal/helpers/chat.go index 532a1584..34a45d0f 100644 --- a/internal/helpers/chat.go +++ b/internal/helpers/chat.go @@ -622,7 +622,7 @@ func defaultChatMessageTimeRange(cmd *cobra.Command, lookback time.Duration) (st } func formatChatMessageListAllTime(ms int64) string { - return time.UnixMilli(ms).In(shanghaiLocation()).Format("2006-01-02 15:04:05") + return time.UnixMilli(ms).Format("2006-01-02 15:04:05") } func chatMessageSearchAdvancedArgs(cmd *cobra.Command) (map[string]any, error) { From def6ed4d2fbb89639e2b20fa9987536fd8932d9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=95=BF=E7=9C=9F?= Date: Thu, 13 Aug 2026 12:16:16 +0800 Subject: [PATCH 08/16] test(chat): align list-all time expectations --- internal/helpers/chat_message_pagination_test.go | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/internal/helpers/chat_message_pagination_test.go b/internal/helpers/chat_message_pagination_test.go index 78eca26c..61ad4c2f 100644 --- a/internal/helpers/chat_message_pagination_test.go +++ b/internal/helpers/chat_message_pagination_test.go @@ -83,7 +83,12 @@ func TestChatMessagePaginationDefaultSinglePageUnchanged(t *testing.T) { args: []string{"message", "list-all", "--start", "2026-08-01 00:00:00", "--end", "2026-08-02 00:00:00"}, server: "", tool: "search_messages_by_time_range", - want: map[string]any{"startTime": "2026-08-01 00:00:00", "endTime": "2026-08-02 00:00:00", "limit": 50, "cursor": "0"}, + want: map[string]any{ + "startTime": formatChatMessageListAllTime(1785513600000), + "endTime": formatChatMessageListAllTime(1785600000000), + "limit": 50, + "cursor": "0", + }, }, { name: "list-by-sender", @@ -276,8 +281,8 @@ func TestChatMessagePaginationDefaultsStartFromExplicitEnd(t *testing.T) { t.Fatalf("startTime = %d, want %d", startMs, wantStartMs) } if tt.wantTool == "search_messages_by_time_range" { - assertStringArg(t, got.args["startTime"], "2025-12-31 00:00:00") - assertStringArg(t, got.args["endTime"], "2026-01-01 00:00:00") + assertStringArg(t, got.args["startTime"], formatChatMessageListAllTime(wantStartMs)) + assertStringArg(t, got.args["endTime"], formatChatMessageListAllTime(endMs)) } }) } @@ -343,7 +348,7 @@ func TestChatMessagePaginationDefaultsEndFromNowWhenOnlyStartProvided(t *testing t.Fatalf("endTime = %d, want between %d and %d", endMs, wantEndMin, wantEndMax) } if tt.wantTool == "search_messages_by_time_range" { - assertStringArg(t, got.args["startTime"], "2026-01-01 00:00:00") + assertStringArg(t, got.args["startTime"], formatChatMessageListAllTime(startMs)) parseChatMessageListAllTimeArg(t, got.args["endTime"]) } }) @@ -423,7 +428,7 @@ func parseChatMessageListAllTimeArg(t *testing.T, value any) time.Time { if strings.Contains(raw, "T") { t.Fatalf("time arg = %q, want yyyy-MM-dd HH:mm:ss without RFC3339 separator", raw) } - parsed, err := time.ParseInLocation("2006-01-02 15:04:05", raw, shanghaiLocation()) + parsed, err := time.ParseInLocation("2006-01-02 15:04:05", raw, time.Local) if err != nil { t.Fatalf("time arg = %q, parse err = %v", raw, err) } From 45a80185f629864cb6a029ea7642f416a45f1b97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=95=BF=E7=9C=9F?= Date: Thu, 13 Aug 2026 14:42:26 +0800 Subject: [PATCH 09/16] fix(chat): pass explicit list-all times through --- .changes/973-chat-message-time-defaults.md | 2 +- internal/helpers/chat.go | 16 +---- .../helpers/chat_message_pagination_test.go | 59 ++++++++++--------- 3 files changed, 36 insertions(+), 41 deletions(-) diff --git a/.changes/973-chat-message-time-defaults.md b/.changes/973-chat-message-time-defaults.md index 1a6e5496..fbe6ba2c 100644 --- a/.changes/973-chat-message-time-defaults.md +++ b/.changes/973-chat-message-time-defaults.md @@ -2,4 +2,4 @@ category: Fixed --- -- **Chat message time defaults** (#973) — validates `chat message list-all` time ranges locally and defaults omitted message-query time bounds without creating reversed windows. +- **Chat message time defaults** (#973) — defaults omitted message-query time bounds without creating reversed windows. diff --git a/internal/helpers/chat.go b/internal/helpers/chat.go index 34a45d0f..c1c7e672 100644 --- a/internal/helpers/chat.go +++ b/internal/helpers/chat.go @@ -471,20 +471,10 @@ func chatMessageListAllArgs(cmd *cobra.Command) (map[string]any, error) { if err != nil { return nil, err } - startMs, err := parseISOTimeToMillis("start", startRaw) - if err != nil { - return nil, err - } - endMs, _ := parseISOTimeToMillis("end", endRaw) - if err := validateTimeRange(startMs, endMs); err != nil { - return nil, err - } - startTime := formatChatMessageListAllTime(startMs) - endTime := formatChatMessageListAllTime(endMs) cursor, _ := cmd.Flags().GetString("cursor") return map[string]any{ - "startTime": startTime, - "endTime": endTime, + "startTime": startRaw, + "endTime": endRaw, "limit": chatIntFlagOrFallback(cmd, "limit", "size"), "cursor": cursor, }, nil @@ -608,7 +598,7 @@ func defaultChatMessageTimeRange(cmd *cobra.Command, lookback time.Duration) (st anchor := time.Now() if endRaw == "" { endRaw = anchor.Format(time.RFC3339) - } else { + } else if startRaw == "" { endMs, err := parseISOTimeToMillis("end", endRaw) if err != nil { return "", "", err diff --git a/internal/helpers/chat_message_pagination_test.go b/internal/helpers/chat_message_pagination_test.go index 61ad4c2f..f373e130 100644 --- a/internal/helpers/chat_message_pagination_test.go +++ b/internal/helpers/chat_message_pagination_test.go @@ -83,12 +83,7 @@ func TestChatMessagePaginationDefaultSinglePageUnchanged(t *testing.T) { args: []string{"message", "list-all", "--start", "2026-08-01 00:00:00", "--end", "2026-08-02 00:00:00"}, server: "", tool: "search_messages_by_time_range", - want: map[string]any{ - "startTime": formatChatMessageListAllTime(1785513600000), - "endTime": formatChatMessageListAllTime(1785600000000), - "limit": 50, - "cursor": "0", - }, + want: map[string]any{"startTime": "2026-08-01 00:00:00", "endTime": "2026-08-02 00:00:00", "limit": 50, "cursor": "0"}, }, { name: "list-by-sender", @@ -281,8 +276,8 @@ func TestChatMessagePaginationDefaultsStartFromExplicitEnd(t *testing.T) { t.Fatalf("startTime = %d, want %d", startMs, wantStartMs) } if tt.wantTool == "search_messages_by_time_range" { - assertStringArg(t, got.args["startTime"], formatChatMessageListAllTime(wantStartMs)) - assertStringArg(t, got.args["endTime"], formatChatMessageListAllTime(endMs)) + assertStringArg(t, got.args["startTime"], time.UnixMilli(wantStartMs).Format(time.RFC3339)) + assertStringArg(t, got.args["endTime"], endRaw) } }) } @@ -348,28 +343,31 @@ func TestChatMessagePaginationDefaultsEndFromNowWhenOnlyStartProvided(t *testing t.Fatalf("endTime = %d, want between %d and %d", endMs, wantEndMin, wantEndMax) } if tt.wantTool == "search_messages_by_time_range" { - assertStringArg(t, got.args["startTime"], formatChatMessageListAllTime(startMs)) + assertStringArg(t, got.args["startTime"], startRaw) parseChatMessageListAllTimeArg(t, got.args["endTime"]) } }) } } -func TestChatMessageListAllRejectsInvalidTimeRanges(t *testing.T) { +func TestChatMessageListAllPassesExplicitTimeBoundsThrough(t *testing.T) { tests := []struct { - name string - args []string - wantErr string + name string + args []string + wantStart string + wantEnd string }{ { - name: "invalid start", - args: []string{"message", "list-all", "--start", "not-a-time", "--end", "2020-01-01T00:00:00+08:00"}, - wantErr: "cannot parse time for --start", + name: "invalid start", + args: []string{"message", "list-all", "--start", "not-a-time", "--end", "2020-01-01T00:00:00+08:00"}, + wantStart: "not-a-time", + wantEnd: "2020-01-01T00:00:00+08:00", }, { - name: "end before start", - args: []string{"message", "list-all", "--start", "2021-01-01T00:00:00+08:00", "--end", "2020-01-01T00:00:00+08:00"}, - wantErr: "--end must be after --start", + name: "end before start", + args: []string{"message", "list-all", "--start", "2021-01-01T00:00:00+08:00", "--end", "2020-01-01T00:00:00+08:00"}, + wantStart: "2021-01-01T00:00:00+08:00", + wantEnd: "2020-01-01T00:00:00+08:00", }, } @@ -377,15 +375,18 @@ func TestChatMessageListAllRejectsInvalidTimeRanges(t *testing.T) { t.Run(tt.name, func(t *testing.T) { caller := &chatMessagePaginationCaller{} _, err := executeChatMessagePaginationCommand(t, caller, tt.args...) - if err == nil { - t.Fatal("command succeeded, want validation error") + if err != nil { + t.Fatal(err) } - if !strings.Contains(err.Error(), tt.wantErr) { - t.Fatalf("error = %v, want containing %q", err, tt.wantErr) + if len(caller.calls) != 1 { + t.Fatalf("calls = %#v, want one MCP call", caller.calls) } - if len(caller.calls) != 0 { - t.Fatalf("calls = %#v, want no MCP call", caller.calls) + got := caller.calls[0] + if got.tool != "search_messages_by_time_range" { + t.Fatalf("tool = %q, want search_messages_by_time_range", got.tool) } + assertStringArg(t, got.args["startTime"], tt.wantStart) + assertStringArg(t, got.args["endTime"], tt.wantEnd) }) } } @@ -423,10 +424,14 @@ func parseChatMessageListAllTimeArg(t *testing.T, value any) time.Time { t.Helper() raw, ok := value.(string) if !ok { - t.Fatalf("time arg = %#v, want yyyy-MM-dd HH:mm:ss string", value) + t.Fatalf("time arg = %#v, want time string", value) } if strings.Contains(raw, "T") { - t.Fatalf("time arg = %q, want yyyy-MM-dd HH:mm:ss without RFC3339 separator", raw) + parsedMs, err := parseISOTimeToMillis("time", raw) + if err != nil { + t.Fatalf("time arg = %q, parse err = %v", raw, err) + } + return time.UnixMilli(parsedMs) } parsed, err := time.ParseInLocation("2006-01-02 15:04:05", raw, time.Local) if err != nil { From 9eeb0681ff2f199984b85a4f17b736465174be17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=95=BF=E7=9C=9F?= Date: Thu, 13 Aug 2026 15:31:35 +0800 Subject: [PATCH 10/16] test(chat): cover list-all time defaults in platform gate --- internal/helpers/chat_command_edges_test.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/internal/helpers/chat_command_edges_test.go b/internal/helpers/chat_command_edges_test.go index cadc2207..2c97ce06 100644 --- a/internal/helpers/chat_command_edges_test.go +++ b/internal/helpers/chat_command_edges_test.go @@ -160,6 +160,9 @@ func TestCrossPlatformCoverageChatCommandValidationAndSuccessEdges(t *testing.T) os.Args = []string{"dws", "chat"} t.Cleanup(func() { deps, os.Args = previousDeps, previousArgs }) caller := &productExampleCaller{} + if got := formatChatMessageListAllTime(0); got == "" { + t.Fatal("formatChatMessageListAllTime returned empty string") + } commands := [][]string{ {"chmod", "chat.read", "--ttl="}, @@ -168,6 +171,10 @@ func TestCrossPlatformCoverageChatCommandValidationAndSuccessEdges(t *testing.T) {"message", "list", "--user=D-user", "--time=2026-01-01", "--limit=1"}, {"message", "list", "--open-dingtalk-id=D-user", "--time=2026-01-01", "--direction=sideways"}, {"message", "list-direct", "--user=D-user", "--limit=1"}, + {"message", "list-all"}, + {"message", "list-all", "--end=2026-01-02T00:00:00Z"}, + {"message", "list-all", "--end=bad"}, + {"message", "list-all", "--start=not-a-time", "--end=2026-01-02T00:00:00Z"}, {"message", "list-all", "--start=2026-01-01 00:00:00", "--end=bad"}, {"message", "list-all", "--start=2026-01-02 00:00:00", "--end=2026-01-01 00:00:00"}, {"message", "list-by-sender", "--sender-user-id=u1", "--start=bad"}, From 35f399e2cf78a72073f5e6a2359d3aa2dd1d5651 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=95=BF=E7=9C=9F?= Date: Thu, 13 Aug 2026 16:16:00 +0800 Subject: [PATCH 11/16] fix(chat): use Shanghai time for message defaults --- internal/helpers/chat.go | 8 +++- internal/helpers/chat_message_search_test.go | 43 ++++++++++++++++++++ 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/internal/helpers/chat.go b/internal/helpers/chat.go index c1c7e672..69af617d 100644 --- a/internal/helpers/chat.go +++ b/internal/helpers/chat.go @@ -615,6 +615,10 @@ func formatChatMessageListAllTime(ms int64) string { return time.UnixMilli(ms).Format("2006-01-02 15:04:05") } +func defaultChatMessageListTime() string { + return time.Now().In(shanghaiLocation()).Format("2006-01-02 15:04:05") +} + func chatMessageSearchAdvancedArgs(cmd *cobra.Command) (map[string]any, error) { toolArgs := map[string]any{} if v := flagOrFallback(cmd, "query", "keyword"); v != "" { @@ -2664,7 +2668,7 @@ func newChatCommand() *cobra.Command { timeVal := mustGetFlag(cmd, "time") defaultForward := true if timeVal == "" { - timeVal = time.Now().Format("2006-01-02 15:04:05") + timeVal = defaultChatMessageListTime() defaultForward = false } forward, err := resolveMessageForward(cmd, defaultForward) @@ -2756,7 +2760,7 @@ func newChatCommand() *cobra.Command { timeVal, _ := cmd.Flags().GetString("time") defaultForward := true if strings.TrimSpace(timeVal) == "" { - timeVal = time.Now().Format("2006-01-02 15:04:05") + timeVal = defaultChatMessageListTime() defaultForward = false } forward, err := resolveMessageForward(cmd, defaultForward) diff --git a/internal/helpers/chat_message_search_test.go b/internal/helpers/chat_message_search_test.go index 340a3bd6..44c68aab 100644 --- a/internal/helpers/chat_message_search_test.go +++ b/internal/helpers/chat_message_search_test.go @@ -724,6 +724,49 @@ func TestCrossPlatformCoverageChatMessageListUsesMCPMetadataGroupKey(t *testing. } } +func TestCrossPlatformCoverageChatMessageListDefaultTimeUsesShanghaiLocation(t *testing.T) { + previousLocal := time.Local + time.Local = time.UTC + t.Cleanup(func() { time.Local = previousLocal }) + + tests := []struct { + name string + args []string + }{ + {name: "group", args: []string{"message", "list", "--group", "cid-1", "--limit", "50"}}, + {name: "direct", args: []string{"message", "list-direct", "--user", "user-1", "--limit", "50"}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + caller := &chatChangedContractCaller{} + before := time.Now() + err := executeChatChangedContract(t, caller, tt.args...) + after := time.Now() + if err != nil { + t.Fatal(err) + } + if len(caller.calls) != 1 { + t.Fatalf("calls = %#v, want one MCP call", caller.calls) + } + raw, ok := caller.calls[0].args["time"].(string) + if !ok || raw == "" { + t.Fatalf("time arg = %#v, want non-empty string", caller.calls[0].args["time"]) + } + got, err := time.ParseInLocation("2006-01-02 15:04:05", raw, shanghaiLocation()) + if err != nil { + t.Fatalf("time arg = %q, parse err = %v", raw, err) + } + if got.Before(before.Add(-time.Second)) || got.After(after.Add(time.Second)) { + t.Fatalf("time arg = %q (%s), want current time in Shanghai between %s and %s", raw, got, before, after) + } + if caller.calls[0].args["forward"] != false { + t.Fatalf("forward = %#v, want false when --time is omitted", caller.calls[0].args["forward"]) + } + }) + } +} + func TestCrossPlatformCoverageChatAuditUsesUserIDs(t *testing.T) { caller := &chatChangedContractCaller{} err := executeChatChangedContract(t, caller, From a32608f96400fcba8aa2af35b84c3341b6122e04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=95=BF=E7=9C=9F?= Date: Thu, 13 Aug 2026 17:38:35 +0800 Subject: [PATCH 12/16] fix(chat): use local time for message defaults --- internal/helpers/chat.go | 23 +++++++++++++++++-- .../helpers/chat_message_pagination_test.go | 22 ++++++++++++++++-- internal/helpers/chat_message_search_test.go | 6 ++--- 3 files changed, 44 insertions(+), 7 deletions(-) diff --git a/internal/helpers/chat.go b/internal/helpers/chat.go index 69af617d..5950772a 100644 --- a/internal/helpers/chat.go +++ b/internal/helpers/chat.go @@ -467,7 +467,7 @@ func validateNativeSearchConversationScope(conversationIDs []string) error { } func chatMessageListAllArgs(cmd *cobra.Command) (map[string]any, error) { - startRaw, endRaw, err := defaultChatMessageTimeRange(cmd, 24*time.Hour) + startRaw, endRaw, err := defaultChatMessageListAllTimeRange(cmd, 24*time.Hour) if err != nil { return nil, err } @@ -480,6 +480,25 @@ func chatMessageListAllArgs(cmd *cobra.Command) (map[string]any, error) { }, nil } +func defaultChatMessageListAllTimeRange(cmd *cobra.Command, lookback time.Duration) (string, string, error) { + startRaw := mustGetFlag(cmd, "start") + endRaw := mustGetFlag(cmd, "end") + anchor := time.Now() + if endRaw == "" { + endRaw = formatChatMessageListAllTime(anchor.UnixMilli()) + } else if startRaw == "" { + endMs, err := parseISOTimeToMillis("end", endRaw) + if err != nil { + return "", "", err + } + anchor = time.UnixMilli(endMs) + } + if startRaw == "" { + startRaw = formatChatMessageListAllTime(anchor.Add(-lookback).UnixMilli()) + } + return startRaw, endRaw, nil +} + func chatMessageListBySenderArgs(cmd *cobra.Command) (map[string]any, error) { senderUserID := flagOrFallback(cmd, "sender-user-id", "sender") senderOpenDingTalkID, _ := cmd.Flags().GetString("sender-open-dingtalk-id") @@ -616,7 +635,7 @@ func formatChatMessageListAllTime(ms int64) string { } func defaultChatMessageListTime() string { - return time.Now().In(shanghaiLocation()).Format("2006-01-02 15:04:05") + return time.Now().Format("2006-01-02 15:04:05") } func chatMessageSearchAdvancedArgs(cmd *cobra.Command) (map[string]any, error) { diff --git a/internal/helpers/chat_message_pagination_test.go b/internal/helpers/chat_message_pagination_test.go index f373e130..e444eb5b 100644 --- a/internal/helpers/chat_message_pagination_test.go +++ b/internal/helpers/chat_message_pagination_test.go @@ -210,6 +210,10 @@ func TestChatMessagePaginationUsesDefaultTimeWindows(t *testing.T) { if startMs < wantStartMin || startMs > wantStartMax { t.Fatalf("startTime = %d, want between %d and %d", startMs, wantStartMin, wantStartMax) } + if tt.wantTool == "search_messages_by_time_range" { + assertChatMessageListAllTimeFormat(t, got.args["startTime"]) + assertChatMessageListAllTimeFormat(t, got.args["endTime"]) + } }) } } @@ -276,7 +280,7 @@ func TestChatMessagePaginationDefaultsStartFromExplicitEnd(t *testing.T) { t.Fatalf("startTime = %d, want %d", startMs, wantStartMs) } if tt.wantTool == "search_messages_by_time_range" { - assertStringArg(t, got.args["startTime"], time.UnixMilli(wantStartMs).Format(time.RFC3339)) + assertStringArg(t, got.args["startTime"], formatChatMessageListAllTime(wantStartMs)) assertStringArg(t, got.args["endTime"], endRaw) } }) @@ -344,7 +348,7 @@ func TestChatMessagePaginationDefaultsEndFromNowWhenOnlyStartProvided(t *testing } if tt.wantTool == "search_messages_by_time_range" { assertStringArg(t, got.args["startTime"], startRaw) - parseChatMessageListAllTimeArg(t, got.args["endTime"]) + assertChatMessageListAllTimeFormat(t, got.args["endTime"]) } }) } @@ -440,6 +444,20 @@ func parseChatMessageListAllTimeArg(t *testing.T, value any) time.Time { return parsed } +func assertChatMessageListAllTimeFormat(t *testing.T, value any) { + t.Helper() + raw, ok := value.(string) + if !ok { + t.Fatalf("time arg = %#v, want time string", value) + } + if strings.Contains(raw, "T") { + t.Fatalf("time arg = %q, want yyyy-MM-dd HH:mm:ss without RFC3339 separator", raw) + } + if _, err := time.ParseInLocation("2006-01-02 15:04:05", raw, time.Local); err != nil { + t.Fatalf("time arg = %q, parse err = %v", raw, err) + } +} + func assertStringArg(t *testing.T, value any, want string) { t.Helper() got, ok := value.(string) diff --git a/internal/helpers/chat_message_search_test.go b/internal/helpers/chat_message_search_test.go index 44c68aab..eeec924e 100644 --- a/internal/helpers/chat_message_search_test.go +++ b/internal/helpers/chat_message_search_test.go @@ -724,7 +724,7 @@ func TestCrossPlatformCoverageChatMessageListUsesMCPMetadataGroupKey(t *testing. } } -func TestCrossPlatformCoverageChatMessageListDefaultTimeUsesShanghaiLocation(t *testing.T) { +func TestCrossPlatformCoverageChatMessageListDefaultTimeUsesLocalLocation(t *testing.T) { previousLocal := time.Local time.Local = time.UTC t.Cleanup(func() { time.Local = previousLocal }) @@ -753,12 +753,12 @@ func TestCrossPlatformCoverageChatMessageListDefaultTimeUsesShanghaiLocation(t * if !ok || raw == "" { t.Fatalf("time arg = %#v, want non-empty string", caller.calls[0].args["time"]) } - got, err := time.ParseInLocation("2006-01-02 15:04:05", raw, shanghaiLocation()) + got, err := time.ParseInLocation("2006-01-02 15:04:05", raw, time.Local) if err != nil { t.Fatalf("time arg = %q, parse err = %v", raw, err) } if got.Before(before.Add(-time.Second)) || got.After(after.Add(time.Second)) { - t.Fatalf("time arg = %q (%s), want current time in Shanghai between %s and %s", raw, got, before, after) + t.Fatalf("time arg = %q (%s), want current local time between %s and %s", raw, got, before, after) } if caller.calls[0].args["forward"] != false { t.Fatalf("forward = %#v, want false when --time is omitted", caller.calls[0].args["forward"]) From 90278ab2fc69215cf41942e405d9774b80193750 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=95=BF=E7=9C=9F?= Date: Thu, 13 Aug 2026 18:01:57 +0800 Subject: [PATCH 13/16] test(chat): cover message default end window --- internal/helpers/chat_command_edges_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/helpers/chat_command_edges_test.go b/internal/helpers/chat_command_edges_test.go index 2c97ce06..93322ff4 100644 --- a/internal/helpers/chat_command_edges_test.go +++ b/internal/helpers/chat_command_edges_test.go @@ -180,6 +180,7 @@ func TestCrossPlatformCoverageChatCommandValidationAndSuccessEdges(t *testing.T) {"message", "list-by-sender", "--sender-user-id=u1", "--start=bad"}, {"message", "list-by-sender", "--sender-user-id=u1", "--start=2026-01-01T00:00:00Z", "--end=bad"}, {"message", "list-by-sender", "--sender-user-id=u1", "--start=2026-01-02T00:00:00Z", "--end=2026-01-01T00:00:00Z"}, + {"message", "list-by-sender", "--sender-user-id=u1", "--end=2026-01-02T00:00:00Z"}, {"message", "list-by-sender", "--sender-user-id=u1", "--start=2026-01-01T00:00:00Z", "--end=2026-01-02T00:00:00Z"}, {"message", "list-by-sender", "--sender-open-dingtalk-id=D1", "--start=2026-01-01T00:00:00Z"}, {"message", "list-mentions", "--start=bad", "--end=2026-01-02T00:00:00Z"}, From c3ffb9c831d4bce71e6050aab4c1c9c705f03ec0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=95=BF=E7=9C=9F?= Date: Thu, 13 Aug 2026 20:56:46 +0800 Subject: [PATCH 14/16] fix(chat): validate list-all time defaults --- .changes/973-chat-message-time-defaults.md | 2 +- internal/helpers/chat.go | 13 ++- internal/helpers/chat_command_edges_test.go | 1 + .../helpers/chat_message_pagination_test.go | 83 +++++++++++++------ 4 files changed, 72 insertions(+), 27 deletions(-) diff --git a/.changes/973-chat-message-time-defaults.md b/.changes/973-chat-message-time-defaults.md index fbe6ba2c..09da6854 100644 --- a/.changes/973-chat-message-time-defaults.md +++ b/.changes/973-chat-message-time-defaults.md @@ -2,4 +2,4 @@ category: Fixed --- -- **Chat message time defaults** (#973) — defaults omitted message-query time bounds without creating reversed windows. +- **Chat message time defaults** (#973) — default omitted `chat message list-all` time bounds in `Asia/Shanghai` when emitting timezone-less `yyyy-MM-dd HH:mm:ss` values, matching parsing semantics and rejecting reversed windows. diff --git a/internal/helpers/chat.go b/internal/helpers/chat.go index 5950772a..0a3a9b27 100644 --- a/internal/helpers/chat.go +++ b/internal/helpers/chat.go @@ -471,6 +471,17 @@ func chatMessageListAllArgs(cmd *cobra.Command) (map[string]any, error) { if err != nil { return nil, err } + startMs, err := parseISOTimeToMillis("start", startRaw) + if err != nil { + return nil, err + } + endMs, err := parseISOTimeToMillis("end", endRaw) + if err != nil { + return nil, err + } + if err := validateTimeRange(startMs, endMs); err != nil { + return nil, err + } cursor, _ := cmd.Flags().GetString("cursor") return map[string]any{ "startTime": startRaw, @@ -631,7 +642,7 @@ func defaultChatMessageTimeRange(cmd *cobra.Command, lookback time.Duration) (st } func formatChatMessageListAllTime(ms int64) string { - return time.UnixMilli(ms).Format("2006-01-02 15:04:05") + return time.UnixMilli(ms).In(shanghaiLocation()).Format("2006-01-02 15:04:05") } func defaultChatMessageListTime() string { diff --git a/internal/helpers/chat_command_edges_test.go b/internal/helpers/chat_command_edges_test.go index 93322ff4..a5f0bacd 100644 --- a/internal/helpers/chat_command_edges_test.go +++ b/internal/helpers/chat_command_edges_test.go @@ -177,6 +177,7 @@ func TestCrossPlatformCoverageChatCommandValidationAndSuccessEdges(t *testing.T) {"message", "list-all", "--start=not-a-time", "--end=2026-01-02T00:00:00Z"}, {"message", "list-all", "--start=2026-01-01 00:00:00", "--end=bad"}, {"message", "list-all", "--start=2026-01-02 00:00:00", "--end=2026-01-01 00:00:00"}, + {"message", "list-all", "--start=2027-01-01 00:00:00"}, {"message", "list-by-sender", "--sender-user-id=u1", "--start=bad"}, {"message", "list-by-sender", "--sender-user-id=u1", "--start=2026-01-01T00:00:00Z", "--end=bad"}, {"message", "list-by-sender", "--sender-user-id=u1", "--start=2026-01-02T00:00:00Z", "--end=2026-01-01T00:00:00Z"}, diff --git a/internal/helpers/chat_message_pagination_test.go b/internal/helpers/chat_message_pagination_test.go index e444eb5b..e947f489 100644 --- a/internal/helpers/chat_message_pagination_test.go +++ b/internal/helpers/chat_message_pagination_test.go @@ -287,6 +287,40 @@ func TestChatMessagePaginationDefaultsStartFromExplicitEnd(t *testing.T) { } } +func TestChatMessageListAllDefaultStartFromTimezoneLessEndUsesShanghai(t *testing.T) { + previousLocal := time.Local + t.Cleanup(func() { time.Local = previousLocal }) + + for _, loc := range []*time.Location{time.UTC, time.FixedZone("EST", -5*3600)} { + t.Run(loc.String(), func(t *testing.T) { + time.Local = loc + caller := &chatMessagePaginationCaller{} + _, err := executeChatMessagePaginationCommand(t, caller, "message", "list-all", "--end", "2026-03-01 00:00:00") + if err != nil { + t.Fatal(err) + } + if len(caller.calls) != 1 { + t.Fatalf("calls = %#v, want one call", caller.calls) + } + got := caller.calls[0] + assertStringArg(t, got.args["endTime"], "2026-03-01 00:00:00") + assertStringArg(t, got.args["startTime"], "2026-02-28 00:00:00") + + startMs, err := parseISOTimeToMillis("start", got.args["startTime"].(string)) + if err != nil { + t.Fatal(err) + } + endMs, err := parseISOTimeToMillis("end", got.args["endTime"].(string)) + if err != nil { + t.Fatal(err) + } + if gotWindow := time.Duration(endMs-startMs) * time.Millisecond; gotWindow != 24*time.Hour { + t.Fatalf("window = %v, want 24h", gotWindow) + } + }) + } +} + func TestChatMessagePaginationDefaultsEndFromNowWhenOnlyStartProvided(t *testing.T) { startRaw := "2026-01-01T00:00:00+08:00" startMs, err := parseISOTimeToMillis("start", startRaw) @@ -354,24 +388,26 @@ func TestChatMessagePaginationDefaultsEndFromNowWhenOnlyStartProvided(t *testing } } -func TestChatMessageListAllPassesExplicitTimeBoundsThrough(t *testing.T) { +func TestChatMessageListAllRejectsInvalidTimeBounds(t *testing.T) { tests := []struct { - name string - args []string - wantStart string - wantEnd string + name string + args []string + wantErr string }{ { - name: "invalid start", - args: []string{"message", "list-all", "--start", "not-a-time", "--end", "2020-01-01T00:00:00+08:00"}, - wantStart: "not-a-time", - wantEnd: "2020-01-01T00:00:00+08:00", + name: "invalid start", + args: []string{"message", "list-all", "--start", "not-a-time", "--end", "2020-01-01T00:00:00+08:00"}, + wantErr: "cannot parse time for --start", }, { - name: "end before start", - args: []string{"message", "list-all", "--start", "2021-01-01T00:00:00+08:00", "--end", "2020-01-01T00:00:00+08:00"}, - wantStart: "2021-01-01T00:00:00+08:00", - wantEnd: "2020-01-01T00:00:00+08:00", + name: "end before start", + args: []string{"message", "list-all", "--start", "2021-01-01T00:00:00+08:00", "--end", "2020-01-01T00:00:00+08:00"}, + wantErr: "--end must be after --start", + }, + { + name: "default end before future start", + args: []string{"message", "list-all", "--start", "2027-01-01 00:00:00"}, + wantErr: "--end must be after --start", }, } @@ -379,18 +415,15 @@ func TestChatMessageListAllPassesExplicitTimeBoundsThrough(t *testing.T) { t.Run(tt.name, func(t *testing.T) { caller := &chatMessagePaginationCaller{} _, err := executeChatMessagePaginationCommand(t, caller, tt.args...) - if err != nil { - t.Fatal(err) + if err == nil { + t.Fatal("expected validation error, got nil") } - if len(caller.calls) != 1 { - t.Fatalf("calls = %#v, want one MCP call", caller.calls) + if !strings.Contains(err.Error(), tt.wantErr) { + t.Fatalf("error = %q, want contains %q", err.Error(), tt.wantErr) } - got := caller.calls[0] - if got.tool != "search_messages_by_time_range" { - t.Fatalf("tool = %q, want search_messages_by_time_range", got.tool) + if len(caller.calls) != 0 { + t.Fatalf("calls = %#v, want no MCP call", caller.calls) } - assertStringArg(t, got.args["startTime"], tt.wantStart) - assertStringArg(t, got.args["endTime"], tt.wantEnd) }) } } @@ -437,11 +470,11 @@ func parseChatMessageListAllTimeArg(t *testing.T, value any) time.Time { } return time.UnixMilli(parsedMs) } - parsed, err := time.ParseInLocation("2006-01-02 15:04:05", raw, time.Local) + parsedMs, err := parseISOTimeToMillis("time", raw) if err != nil { t.Fatalf("time arg = %q, parse err = %v", raw, err) } - return parsed + return time.UnixMilli(parsedMs) } func assertChatMessageListAllTimeFormat(t *testing.T, value any) { @@ -453,7 +486,7 @@ func assertChatMessageListAllTimeFormat(t *testing.T, value any) { if strings.Contains(raw, "T") { t.Fatalf("time arg = %q, want yyyy-MM-dd HH:mm:ss without RFC3339 separator", raw) } - if _, err := time.ParseInLocation("2006-01-02 15:04:05", raw, time.Local); err != nil { + if _, err := parseISOTimeToMillis("time", raw); err != nil { t.Fatalf("time arg = %q, parse err = %v", raw, err) } } From 113e084a8d447593d4e5cf04058be0e48a238b42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=95=BF=E7=9C=9F?= Date: Thu, 13 Aug 2026 22:31:57 +0800 Subject: [PATCH 15/16] fix(chat): align default message time timezone --- internal/helpers/chat.go | 2 +- internal/helpers/chat_message_search_test.go | 112 ++++++++++++++----- 2 files changed, 82 insertions(+), 32 deletions(-) diff --git a/internal/helpers/chat.go b/internal/helpers/chat.go index 0a3a9b27..770e43c9 100644 --- a/internal/helpers/chat.go +++ b/internal/helpers/chat.go @@ -646,7 +646,7 @@ func formatChatMessageListAllTime(ms int64) string { } func defaultChatMessageListTime() string { - return time.Now().Format("2006-01-02 15:04:05") + return time.Now().In(shanghaiLocation()).Format("2006-01-02 15:04:05") } func chatMessageSearchAdvancedArgs(cmd *cobra.Command) (map[string]any, error) { diff --git a/internal/helpers/chat_message_search_test.go b/internal/helpers/chat_message_search_test.go index eeec924e..bdf1da88 100644 --- a/internal/helpers/chat_message_search_test.go +++ b/internal/helpers/chat_message_search_test.go @@ -724,44 +724,94 @@ func TestCrossPlatformCoverageChatMessageListUsesMCPMetadataGroupKey(t *testing. } } -func TestCrossPlatformCoverageChatMessageListDefaultTimeUsesLocalLocation(t *testing.T) { +func TestCrossPlatformCoverageChatMessageListDefaultTimeUsesShanghaiLocation(t *testing.T) { previousLocal := time.Local - time.Local = time.UTC t.Cleanup(func() { time.Local = previousLocal }) tests := []struct { - name string - args []string + name string + args []string + wantTool string + wantTarget map[string]any }{ - {name: "group", args: []string{"message", "list", "--group", "cid-1", "--limit", "50"}}, - {name: "direct", args: []string{"message", "list-direct", "--user", "user-1", "--limit", "50"}}, + { + name: "group", + args: []string{"message", "list", "--group", "cid-1", "--limit", "50"}, + wantTool: "list_conversation_message_v2", + wantTarget: map[string]any{"openconversation_id": "cid-1"}, + }, + { + name: "user", + args: []string{"message", "list", "--user", "user-1", "--limit", "50"}, + wantTool: "list_individual_chat_message", + wantTarget: map[string]any{"userId": "user-1"}, + }, + { + name: "user open DingTalk ID fallback", + args: []string{"message", "list", "--user", "D-user", "--limit", "50"}, + wantTool: "list_individual_chat_message", + wantTarget: map[string]any{"openDingTalkId": "D-user"}, + }, + { + name: "open DingTalk ID", + args: []string{"message", "list", "--open-dingtalk-id", "D-open", "--limit", "50"}, + wantTool: "list_individual_chat_message", + wantTarget: map[string]any{"openDingTalkId": "D-open"}, + }, + { + name: "direct user", + args: []string{"message", "list-direct", "--user", "user-1", "--limit", "50"}, + wantTool: "list_individual_chat_message", + wantTarget: map[string]any{"userId": "user-1"}, + }, + { + name: "direct open DingTalk ID", + args: []string{"message", "list-direct", "--open-dingtalk-id", "D-direct", "--limit", "50"}, + wantTool: "list_individual_chat_message", + wantTarget: map[string]any{"openDingTalkId": "D-direct"}, + }, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - caller := &chatChangedContractCaller{} - before := time.Now() - err := executeChatChangedContract(t, caller, tt.args...) - after := time.Now() - if err != nil { - t.Fatal(err) - } - if len(caller.calls) != 1 { - t.Fatalf("calls = %#v, want one MCP call", caller.calls) - } - raw, ok := caller.calls[0].args["time"].(string) - if !ok || raw == "" { - t.Fatalf("time arg = %#v, want non-empty string", caller.calls[0].args["time"]) - } - got, err := time.ParseInLocation("2006-01-02 15:04:05", raw, time.Local) - if err != nil { - t.Fatalf("time arg = %q, parse err = %v", raw, err) - } - if got.Before(before.Add(-time.Second)) || got.After(after.Add(time.Second)) { - t.Fatalf("time arg = %q (%s), want current local time between %s and %s", raw, got, before, after) - } - if caller.calls[0].args["forward"] != false { - t.Fatalf("forward = %#v, want false when --time is omitted", caller.calls[0].args["forward"]) + for _, loc := range []*time.Location{time.UTC, time.FixedZone("EST", -5*3600)} { + t.Run(loc.String(), func(t *testing.T) { + time.Local = loc + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + caller := &chatChangedContractCaller{} + before := time.Now() + err := executeChatChangedContract(t, caller, tt.args...) + after := time.Now() + if err != nil { + t.Fatal(err) + } + if len(caller.calls) != 1 { + t.Fatalf("calls = %#v, want one MCP call", caller.calls) + } + if caller.calls[0].toolName != tt.wantTool { + t.Fatalf("tool = %q, want %q", caller.calls[0].toolName, tt.wantTool) + } + for key, want := range tt.wantTarget { + if got := caller.calls[0].args[key]; got != want { + t.Fatalf("arg %s = %#v, want %#v", key, got, want) + } + } + raw, ok := caller.calls[0].args["time"].(string) + if !ok || raw == "" { + t.Fatalf("time arg = %#v, want non-empty string", caller.calls[0].args["time"]) + } + gotMs, err := parseISOTimeToMillis("time", raw) + if err != nil { + t.Fatalf("time arg = %q, parse err = %v", raw, err) + } + wantMin := before.Add(-time.Second).UnixMilli() + wantMax := after.Add(time.Second).UnixMilli() + if gotMs < wantMin || gotMs > wantMax { + t.Fatalf("time arg = %q (%d), want between %d and %d", raw, gotMs, wantMin, wantMax) + } + if caller.calls[0].args["forward"] != false { + t.Fatalf("forward = %#v, want false when --time is omitted", caller.calls[0].args["forward"]) + } + }) } }) } From 6c78db7467d17035e049ea5794656cd4dc224ec6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=95=BF=E7=9C=9F?= Date: Thu, 13 Aug 2026 23:36:53 +0800 Subject: [PATCH 16/16] fix(chat): document Shanghai time message default --- internal/helpers/chat.go | 2 +- internal/helpers/chat_message_help_test.go | 4 ++-- internal/helpers/im_read_result_contract_test.go | 7 ++++--- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/internal/helpers/chat.go b/internal/helpers/chat.go index 267574de..f36dd104 100644 --- a/internal/helpers/chat.go +++ b/internal/helpers/chat.go @@ -2663,7 +2663,7 @@ func newChatCommand() *cobra.Command { chatMessageListCmd := &cobra.Command{ Use: "list", Short: "拉取会话消息内容", - Long: `拉取指定群聊或单聊的会话消息内容。输出顶层 messages,稳定字段为 messageId 和 text;兼容保留 openMessageId 和 content。--group 指定群聊,--user 指定单聊用户(userId),--open-dingtalk-id 指定单聊用户(openDingTalkId),三者互斥。--time 可选,不传时默认当前时间并向旧消息拉取。推荐使用 --direction newer/older 控制时间方向:newer 表示从给定时间往现在拉,older 表示从给定时间往以前拉。hasMore=true 时用结果中的边界 createTime 作为下次 --time 翻页。引用回复消息会返回 quotedMessage 引用上下文;被引用的原消息是合并转发或图片时,对应的类型与内容也会随引用上下文返回。如果返回的会话消息中包含 openConvThreadId 字段,说明是话题消息,可以调用 dws chat message list-topic-replies 拉取话题回复消息列表,openConvThreadId 作为 topic-id 参数。`, + Long: `拉取指定群聊或单聊的会话消息内容。输出顶层 messages,稳定字段为 messageId 和 text;兼容保留 openMessageId 和 content。--group 指定群聊,--user 指定单聊用户(userId),--open-dingtalk-id 指定单聊用户(openDingTalkId),三者互斥。--time 可选,不传时默认上海时间当前时间并向旧消息拉取。推荐使用 --direction newer/older 控制时间方向:newer 表示从给定时间往现在拉,older 表示从给定时间往以前拉。hasMore=true 时用结果中的边界 createTime 作为下次 --time 翻页。引用回复消息会返回 quotedMessage 引用上下文;被引用的原消息是合并转发或图片时,对应的类型与内容也会随引用上下文返回。如果返回的会话消息中包含 openConvThreadId 字段,说明是话题消息,可以调用 dws chat message list-topic-replies 拉取话题回复消息列表,openConvThreadId 作为 topic-id 参数。`, Example: ` dws chat message list --group dws chat message list --group --time "2025-03-01 00:00:00" dws chat message list --user --time "2025-03-01 00:00:00" --limit 50 diff --git a/internal/helpers/chat_message_help_test.go b/internal/helpers/chat_message_help_test.go index 6941e2e8..9e528d3c 100644 --- a/internal/helpers/chat_message_help_test.go +++ b/internal/helpers/chat_message_help_test.go @@ -102,8 +102,8 @@ func TestCrossPlatformCoverageChatMessageHelpDocumentsOptionalTimeDefaults(t *te name: "list", args: []string{"message", "list", "--help"}, contains: []string{ - "--time 可选,不传时默认当前时间并向旧消息拉取", - "默认当前时间", + "--time 可选,不传时默认上海时间当前时间并向旧消息拉取", + "默认上海时间当前时间", "未传 --time 时默认 older", }, absent: []string{"开始时间,格式: yyyy-MM-dd HH:mm:ss (必填)"}, diff --git a/internal/helpers/im_read_result_contract_test.go b/internal/helpers/im_read_result_contract_test.go index 0d983925..9494069d 100644 --- a/internal/helpers/im_read_result_contract_test.go +++ b/internal/helpers/im_read_result_contract_test.go @@ -213,11 +213,12 @@ func TestCrossPlatformCoverageChatMessageListProjectsStableFieldsAndPreservesLeg 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() + before := time.Now().In(loc) _, err := executeIMReadCommand(t, caller, []string{"dws", "chat"}, newChatCommand, "message", "list", "--group", "cid-1", "--limit", "50") - after := time.Now() + after := time.Now().In(loc) if err != nil { t.Fatalf("chat message list returned error: %v", err) } @@ -231,7 +232,7 @@ func TestCrossPlatformCoverageChatMessageListDefaultsTimeAndOlderDirection(t *te 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, time.Local) + gotTime, err := time.ParseInLocation("2006-01-02 15:04:05", timeRaw, loc) if err != nil { t.Fatalf("parse generated time %q: %v", timeRaw, err) }