将 test/pre-mcp-discovery 分支上多位同学的能力改动整体合入 main, 服务发现端点保持生产 https://mcp.dingtalk.com,未携带分支上的预发改动。 主要内容: - 修雨域(chat / report / todo / contact / mail)对齐 wukong 基线 (#355) - DWS 内容生产能力增强 (#387)、文档导出与 OA 对齐 (#362 #388 #390) - envelope 注册 cli.Aliases 为 cobra aliases、命令结构与 JSON 解析优化 - skills 多包同步 wukong 对齐内容(attendance/calendar/minutes/oa/sheet 等) - product/oa 进一步对齐 wukong 排除项:未携带 566b4e1(test: point service discovery to pre-mcp.dingtalk.com); 以下 4 个常量保持生产:skill_command.go / auth/endpoints.go / cli/loader.go / market/registry.go。 Co-authored-by: john <32427341+wqyenjoy@users.noreply.github.com> Co-authored-by: wxianfeng <wang.fl1429@gmail.com>
154 lines
4.3 KiB
Go
154 lines
4.3 KiB
Go
// Copyright 2026 Alibaba Group
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package paging
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// stubFetcher 用一个预设的 page 序列模拟翻页响应。
|
|
type stubFetcher struct {
|
|
pages []Page
|
|
pageErr map[int]error // 第几次调用要返回 err(从 0 开始)
|
|
calls int
|
|
lastInput string
|
|
}
|
|
|
|
func (s *stubFetcher) Fetch(ctx context.Context, cursor string) (Page, error) {
|
|
s.lastInput = cursor
|
|
idx := s.calls
|
|
s.calls++
|
|
if err, ok := s.pageErr[idx]; ok {
|
|
return Page{}, err
|
|
}
|
|
if idx >= len(s.pages) {
|
|
// 模拟服务端没数据:空 page + 空 cursor
|
|
return Page{}, nil
|
|
}
|
|
return s.pages[idx], nil
|
|
}
|
|
|
|
func TestFetchAll_SinglePage(t *testing.T) {
|
|
s := &stubFetcher{
|
|
pages: []Page{
|
|
{Records: []any{"a", "b", "c"}, NextCursor: ""},
|
|
},
|
|
}
|
|
got := FetchAll(context.Background(), s.Fetch, Options{InterPageDelay: 1 * time.Millisecond})
|
|
if got.HasMore || got.Pages != 1 || len(got.Records) != 3 {
|
|
t.Fatalf("single page: got=%+v", got)
|
|
}
|
|
}
|
|
|
|
func TestFetchAll_MultiPage(t *testing.T) {
|
|
s := &stubFetcher{
|
|
pages: []Page{
|
|
{Records: []any{1, 2}, NextCursor: "c1"},
|
|
{Records: []any{3, 4}, NextCursor: "c2"},
|
|
{Records: []any{5}, NextCursor: ""},
|
|
},
|
|
}
|
|
got := FetchAll(context.Background(), s.Fetch, Options{InterPageDelay: 1 * time.Millisecond})
|
|
if got.HasMore || got.Pages != 3 || len(got.Records) != 5 {
|
|
t.Fatalf("multi page: got=%+v", got)
|
|
}
|
|
}
|
|
|
|
func TestFetchAll_PageLimit(t *testing.T) {
|
|
s := &stubFetcher{
|
|
pages: []Page{
|
|
{Records: []any{1}, NextCursor: "c1"},
|
|
{Records: []any{2}, NextCursor: "c2"},
|
|
{Records: []any{3}, NextCursor: "c3"},
|
|
{Records: []any{4}, NextCursor: "c4"},
|
|
},
|
|
}
|
|
got := FetchAll(context.Background(), s.Fetch, Options{
|
|
PageLimit: 2,
|
|
InterPageDelay: 1 * time.Millisecond,
|
|
})
|
|
if !got.HasMore || got.Pages != 2 || got.LastCursor != "c2" {
|
|
t.Fatalf("page limit: got=%+v", got)
|
|
}
|
|
if !errors.Is(got.Err, ErrPageLimitReached) {
|
|
t.Fatalf("expected ErrPageLimitReached, got %v", got.Err)
|
|
}
|
|
if len(got.Records) != 2 {
|
|
t.Fatalf("page limit records: got=%v", got.Records)
|
|
}
|
|
}
|
|
|
|
func TestFetchAll_MidErrorPartial(t *testing.T) {
|
|
upstreamErr := errors.New("502 bad gateway")
|
|
s := &stubFetcher{
|
|
pages: []Page{
|
|
{Records: []any{1}, NextCursor: "c1"},
|
|
},
|
|
pageErr: map[int]error{1: upstreamErr}, // 第二次调用挂
|
|
}
|
|
got := FetchAll(context.Background(), s.Fetch, Options{InterPageDelay: 1 * time.Millisecond})
|
|
if !got.Partial || !errors.Is(got.Err, upstreamErr) {
|
|
t.Fatalf("partial: got=%+v", got)
|
|
}
|
|
if len(got.Records) != 1 || got.LastCursor != "c1" {
|
|
t.Fatalf("partial preserves data + cursor: got=%+v", got)
|
|
}
|
|
if !got.HasMore {
|
|
t.Fatalf("partial should signal HasMore=true so caller can retry")
|
|
}
|
|
}
|
|
|
|
func TestFetchAll_InitialCursorResume(t *testing.T) {
|
|
s := &stubFetcher{
|
|
pages: []Page{
|
|
{Records: []any{"resumed"}, NextCursor: ""},
|
|
},
|
|
}
|
|
got := FetchAll(context.Background(), s.Fetch, Options{InitialCursor: "saved-from-last-time"})
|
|
if s.lastInput != "saved-from-last-time" {
|
|
t.Fatalf("InitialCursor not propagated: got %q", s.lastInput)
|
|
}
|
|
if len(got.Records) != 1 || got.HasMore {
|
|
t.Fatalf("resume: got=%+v", got)
|
|
}
|
|
}
|
|
|
|
func TestFetchAll_ContextCancel(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel() // 立即取消
|
|
s := &stubFetcher{pages: []Page{{Records: []any{1}, NextCursor: "c1"}}}
|
|
got := FetchAll(ctx, s.Fetch, Options{InterPageDelay: 1 * time.Millisecond})
|
|
if !errors.Is(got.Err, context.Canceled) {
|
|
t.Fatalf("expected context.Canceled, got %v", got.Err)
|
|
}
|
|
if !got.Partial {
|
|
t.Fatalf("partial on cancel: got=%+v", got)
|
|
}
|
|
}
|
|
|
|
func TestFetchAll_UnlimitedPageLimit(t *testing.T) {
|
|
// PageLimit 显式置 -1 时(约定为"不限"),翻完为止
|
|
// (当前实现 PageLimit==0 用默认值 50;要真正无限制需另设特殊值,
|
|
// 这里测试默认 50 上限内能完成全量)
|
|
pages := make([]Page, 10)
|
|
for i := range pages {
|
|
next := ""
|
|
if i < len(pages)-1 {
|
|
next = "c" + string(rune('0'+i))
|
|
}
|
|
pages[i] = Page{Records: []any{i}, NextCursor: next}
|
|
}
|
|
s := &stubFetcher{pages: pages}
|
|
got := FetchAll(context.Background(), s.Fetch, Options{
|
|
PageLimit: 100, // 高于实际页数
|
|
InterPageDelay: 1 * time.Millisecond,
|
|
})
|
|
if got.HasMore || got.Pages != 10 || len(got.Records) != 10 {
|
|
t.Fatalf("unlimited: got=%+v", got)
|
|
}
|
|
}
|