485 lines
18 KiB
Go
485 lines
18 KiB
Go
// Copyright 2026 Alibaba Group
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
package chat
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/helpers"
|
|
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut"
|
|
)
|
|
|
|
func TestCrossPlatformCoverageChatListAllPageAllUsesNumericCursorAndDeduplicates(t *testing.T) {
|
|
fake := &larkAlignmentCaller{sequenceResponses: map[string][]string{
|
|
"im/list_my_groups_pagination": {
|
|
`{"result":{"groups":[{"openConversationId":"g1","title":"群一"}],"hasMore":true,"nextCursor":88}}`,
|
|
`{"result":{"groups":[{"openConversationId":"g1","title":"重复群"},{"openConversationId":"g2","title":"群二"}],"hasMore":false}}`,
|
|
},
|
|
}}
|
|
helpers.InitDeps(fake)
|
|
root := newPlatformCoverageRoot()
|
|
var output bytes.Buffer
|
|
root.SetOut(&output)
|
|
root.SetArgs([]string{"chat", "+chat-list-all", "--limit", "1", "--page-all", "--page-limit", "5"})
|
|
if err := root.Execute(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(fake.calls) != 2 || fake.calls[1].args["cursor"] != float64(88) {
|
|
t.Fatalf("calls = %#v", fake.calls)
|
|
}
|
|
var payload map[string]any
|
|
if err := json.Unmarshal(output.Bytes(), &payload); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if payload["count"] != float64(2) || payload["pagesFetched"] != float64(2) ||
|
|
payload["complete"] != true || payload["hasMore"] != false || payload["failedCount"] != float64(0) {
|
|
t.Fatalf("payload = %#v", payload)
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageDirectMessagesPageAllUsesMillisecondCursor(t *testing.T) {
|
|
const cursorMillis int64 = 1785919699136
|
|
fake := &larkAlignmentCaller{sequenceResponses: map[string][]string{
|
|
"chat/list_individual_chat_message": {
|
|
`{"result":{"messages":[{"openMessageId":"m2","createTime":"2026-08-05 16:48:19"}],"hasMore":true,"nextCursor":1785919699136}}`,
|
|
`{"result":{"messages":[{"openMessageId":"m1","createTime":"2026-08-05 16:48:19"}],"hasMore":false}}`,
|
|
},
|
|
}}
|
|
helpers.InitDeps(fake)
|
|
root := newPlatformCoverageRoot()
|
|
var output bytes.Buffer
|
|
root.SetOut(&output)
|
|
root.SetArgs([]string{
|
|
"chat", "+messages-list-direct", "--open-dingtalk-id", fixtureCurrentDOpenID,
|
|
"--time", "2026-08-05 16:49:00", "--forward=false", "--limit", "1",
|
|
"--page-all", "--page-limit", "5",
|
|
})
|
|
if err := root.Execute(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wantBoundary := time.UnixMilli(cursorMillis).UTC().Format(time.RFC3339Nano)
|
|
if len(fake.calls) != 2 || fake.calls[1].args["time"] != wantBoundary {
|
|
t.Fatalf("calls = %#v", fake.calls)
|
|
}
|
|
var payload map[string]any
|
|
if err := json.Unmarshal(output.Bytes(), &payload); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if payload["count"] != float64(2) || payload["pagesFetched"] != float64(2) ||
|
|
payload["complete"] != true || payload["failedCount"] != float64(0) {
|
|
t.Fatalf("payload = %#v", payload)
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageDirectMessagesPageLimitPublishesExecutableContinuation(t *testing.T) {
|
|
fake := &larkAlignmentCaller{responses: map[string]string{
|
|
"chat/list_individual_chat_message": `{"result":{"messages":[{"openMessageId":"m1"}],"hasMore":true,"nextCursor":1785919699136}}`,
|
|
}}
|
|
helpers.InitDeps(fake)
|
|
root := newPlatformCoverageRoot()
|
|
var output bytes.Buffer
|
|
root.SetOut(&output)
|
|
root.SetArgs([]string{
|
|
"chat", "+messages-list-direct", "--user", "u1", "--time", "2026-08-05 16:49:00",
|
|
"--page-all", "--page-limit", "1",
|
|
})
|
|
if err := root.Execute(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var payload map[string]any
|
|
if err := json.Unmarshal(output.Bytes(), &payload); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if payload["complete"] != false || payload["hasMore"] != true ||
|
|
payload["truncated"] != true || payload["truncatedByPageLimit"] != true || payload["stopReason"] != "page_limit" || payload["nextPage"] == nil {
|
|
t.Fatalf("payload = %#v", payload)
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageChatListAllMaxItemsPublishesStableTruncation(t *testing.T) {
|
|
fake := &larkAlignmentCaller{responses: map[string]string{
|
|
"im/list_my_groups_pagination": `{"result":{"groups":[{"openConversationId":"g1"}],"hasMore":true,"nextCursor":88}}`,
|
|
}}
|
|
helpers.InitDeps(fake)
|
|
root := newPlatformCoverageRoot()
|
|
var output bytes.Buffer
|
|
root.SetOut(&output)
|
|
root.SetArgs([]string{"chat", "+chat-list-all", "--page-all", "--max-items", "1", "--page-delay", "0"})
|
|
if err := root.Execute(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var payload map[string]any
|
|
if err := json.Unmarshal(output.Bytes(), &payload); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if payload["count"] != float64(1) || payload["truncated"] != true ||
|
|
payload["truncatedByResultLimit"] != true || payload["stopReason"] != "result_limit" {
|
|
t.Fatalf("payload = %#v", payload)
|
|
}
|
|
if len(fake.calls) != 1 || fake.calls[0].args["limit"] != 1 || payload["nextCursor"] != float64(88) {
|
|
t.Fatalf("unsafe continuation: calls=%#v payload=%#v", fake.calls, payload)
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageChatListAllFailsClosedOnOversizeAndCanceledDelay(t *testing.T) {
|
|
t.Run("oversized lower page", func(t *testing.T) {
|
|
fake := &larkAlignmentCaller{responses: map[string]string{
|
|
"im/list_my_groups_pagination": `{"result":{"groups":[{"openConversationId":"g1"},{"openConversationId":"g2"}],"hasMore":true,"nextCursor":88}}`,
|
|
}}
|
|
helpers.InitDeps(fake)
|
|
root := newPlatformCoverageRoot()
|
|
var output bytes.Buffer
|
|
root.SetOut(&output)
|
|
root.SetArgs([]string{"chat", "+chat-list-all", "--page-all", "--max-items", "1"})
|
|
if err := root.Execute(); err == nil {
|
|
t.Fatal("oversized lower page unexpectedly published a continuation")
|
|
}
|
|
var payload map[string]any
|
|
if err := json.Unmarshal(output.Bytes(), &payload); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if payload["stopReason"] != "pagination_error" || payload["failedCount"] != float64(1) {
|
|
t.Fatalf("payload = %#v", payload)
|
|
}
|
|
if len(fake.calls) != 1 || fake.calls[0].args["limit"] != 1 || payload["nextCursor"] != nil {
|
|
t.Fatalf("unsafe continuation: calls=%#v payload=%#v", fake.calls, payload)
|
|
}
|
|
})
|
|
|
|
t.Run("canceled delay", func(t *testing.T) {
|
|
fake := &larkAlignmentCaller{responses: map[string]string{
|
|
"im/list_my_groups_pagination": `{"result":{"groups":[{"openConversationId":"g1"}],"hasMore":true,"nextCursor":88}}`,
|
|
}}
|
|
helpers.InitDeps(fake)
|
|
root := newPlatformCoverageRoot()
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
root.SetContext(ctx)
|
|
var output bytes.Buffer
|
|
root.SetOut(&output)
|
|
root.SetArgs([]string{"chat", "+chat-list-all", "--page-all", "--page-delay", "1"})
|
|
if err := root.Execute(); err == nil {
|
|
t.Fatal("canceled delay unexpectedly succeeded")
|
|
}
|
|
var payload map[string]any
|
|
if err := json.Unmarshal(output.Bytes(), &payload); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if payload["stopReason"] != "delay_interrupted" || payload["failedCount"] != float64(1) {
|
|
t.Fatalf("payload = %#v", payload)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestCrossPlatformCoverageDirectMessagesPageAllFailsClosedWithoutMillisecondCursor(t *testing.T) {
|
|
fake := &larkAlignmentCaller{responses: map[string]string{
|
|
"chat/list_individual_chat_message": `{"result":{"messages":[{"openMessageId":"m1"}],"hasMore":true}}`,
|
|
}}
|
|
helpers.InitDeps(fake)
|
|
root := newPlatformCoverageRoot()
|
|
var output bytes.Buffer
|
|
root.SetOut(&output)
|
|
root.SetArgs([]string{
|
|
"chat", "+messages-list-direct", "--user", "u1", "--time", "2026-08-05 16:49:00", "--page-all",
|
|
})
|
|
if err := root.Execute(); err == nil {
|
|
t.Fatal("missing direct-message cursor unexpectedly succeeded")
|
|
}
|
|
var payload map[string]any
|
|
if err := json.Unmarshal(output.Bytes(), &payload); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if payload["complete"] != false || payload["failedCount"] != float64(1) || payload["stopReason"] != "pagination_error" {
|
|
t.Fatalf("payload = %#v", payload)
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageChatListAllLaterReadFailureKeepsPartialLedger(t *testing.T) {
|
|
fake := &larkAlignmentCaller{
|
|
sequenceResponses: map[string][]string{
|
|
"im/list_my_groups_pagination": {`{"result":{"groups":[{"openConversationId":"g1"}],"hasMore":true,"nextCursor":88}}`},
|
|
},
|
|
failProductToolAt: map[string]int{"im/list_my_groups_pagination": 2},
|
|
}
|
|
helpers.InitDeps(fake)
|
|
root := newPlatformCoverageRoot()
|
|
var output bytes.Buffer
|
|
root.SetOut(&output)
|
|
root.SetArgs([]string{"chat", "+chat-list-all", "--page-all"})
|
|
if err := root.Execute(); err == nil {
|
|
t.Fatal("later group-list failure unexpectedly succeeded")
|
|
}
|
|
var payload map[string]any
|
|
if err := json.Unmarshal(output.Bytes(), &payload); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if payload["count"] != float64(1) || payload["complete"] != false || payload["partial"] != true ||
|
|
payload["failedCount"] != float64(1) || payload["stopReason"] != "read_failure" {
|
|
t.Fatalf("payload = %#v", payload)
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageChatRemainingPaginationValidation(t *testing.T) {
|
|
for _, args := range [][]string{
|
|
{"chat", "+chat-list-all", "--limit", "201"},
|
|
{"chat", "+chat-list-all", "--page-limit", "2"},
|
|
{"chat", "+chat-list-all", "--page-all", "--page-limit", "501"},
|
|
{"chat", "+messages-list-direct", "--user", "u1", "--time", "2026-01-01", "--page-limit", "2"},
|
|
{"chat", "+messages-list-direct", "--user", "u1", "--time", "2026-01-01", "--page-all", "--page-limit", "501"},
|
|
} {
|
|
helpers.InitDeps(&larkAlignmentCaller{})
|
|
root := newPlatformCoverageRoot()
|
|
root.SetArgs(args)
|
|
if err := root.Execute(); err == nil {
|
|
t.Fatalf("invalid args succeeded: %v", args)
|
|
}
|
|
}
|
|
root := newPlatformCoverageRoot()
|
|
cmd, _, err := root.Find([]string{"chat", "+chat-list-all"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := cmd.Flags().Set("max-items", "1"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := validateChatListAll(shortcut.RuntimeContextForTest(cmd, ChatListAll)); err == nil {
|
|
t.Fatal("direct auto-page validation unexpectedly succeeded")
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageChatListAllAdditionalEdges(t *testing.T) {
|
|
run := func(t *testing.T, fake *larkAlignmentCaller, args ...string) (map[string]any, error) {
|
|
t.Helper()
|
|
helpers.InitDeps(fake)
|
|
root := newPlatformCoverageRoot()
|
|
var output bytes.Buffer
|
|
root.SetOut(&output)
|
|
root.SetArgs(append([]string{"chat", "+chat-list-all"}, args...))
|
|
err := root.Execute()
|
|
if output.Len() == 0 {
|
|
return nil, err
|
|
}
|
|
var payload map[string]any
|
|
if decodeErr := json.Unmarshal(output.Bytes(), &payload); decodeErr != nil {
|
|
t.Fatal(decodeErr)
|
|
}
|
|
return payload, err
|
|
}
|
|
|
|
t.Run("single page terminal and continuing", func(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
response string
|
|
stop string
|
|
}{
|
|
{name: "terminal", response: `{"result":{"groups":[],"hasMore":false}}`, stop: "source_complete"},
|
|
{name: "continuing", response: `{"result":{"groups":[],"hasMore":true,"nextCursor":2}}`, stop: "single_page"},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
payload, err := run(t, &larkAlignmentCaller{responses: map[string]string{"im/list_my_groups_pagination": tc.response}})
|
|
if err != nil || payload["stopReason"] != tc.stop || payload["pagesFetched"] != float64(1) {
|
|
t.Fatalf("payload=%#v err=%v", payload, err)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("first read failure", func(t *testing.T) {
|
|
for _, args := range [][]string{nil, {"--page-all"}} {
|
|
_, err := run(t, &larkAlignmentCaller{failProductToolAt: map[string]int{"im/list_my_groups_pagination": 1}}, args...)
|
|
if err == nil {
|
|
t.Fatalf("first read failure unexpectedly succeeded for %v", args)
|
|
}
|
|
}
|
|
})
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
response string
|
|
args []string
|
|
wantErr bool
|
|
stop string
|
|
}{
|
|
{name: "unknown pagination", response: `{"result":{"groups":[]}}`, args: []string{"--page-all"}, wantErr: true, stop: "pagination_error"},
|
|
{name: "missing continuation", response: `{"result":{"groups":[],"hasMore":true}}`, args: []string{"--page-all"}, wantErr: true, stop: "pagination_error"},
|
|
{name: "page limit", response: `{"result":{"groups":[{"openConversationId":"g1"}],"hasMore":true,"nextCursor":2}}`, args: []string{"--page-all", "--page-limit", "1"}, stop: "page_limit"},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
payload, err := run(t, &larkAlignmentCaller{responses: map[string]string{"im/list_my_groups_pagination": tc.response}}, tc.args...)
|
|
if (err != nil) != tc.wantErr || payload["stopReason"] != tc.stop {
|
|
t.Fatalf("payload=%#v err=%v", payload, err)
|
|
}
|
|
})
|
|
}
|
|
|
|
t.Run("output errors propagate", func(t *testing.T) {
|
|
for _, args := range [][]string{nil, {"--page-all"}} {
|
|
fake := &larkAlignmentCaller{responses: map[string]string{
|
|
"im/list_my_groups_pagination": `{"result":{"groups":[],"hasMore":false}}`,
|
|
}}
|
|
helpers.InitDeps(fake)
|
|
root := newPlatformCoverageRoot()
|
|
root.SetOut(chatOutputErrorWriter{err: errors.New("fixture output")})
|
|
root.SetArgs(append([]string{"chat", "+chat-list-all"}, args...))
|
|
if err := root.Execute(); err == nil {
|
|
t.Fatalf("output error swallowed for args %v", args)
|
|
}
|
|
}
|
|
})
|
|
|
|
for _, value := range []any{nil, "", "0", "<nil>"} {
|
|
if got := chatListAllCursorString(value); got != "" {
|
|
t.Fatalf("cursor %v = %q", value, got)
|
|
}
|
|
}
|
|
payload, err := run(t, &larkAlignmentCaller{responses: map[string]string{
|
|
"im/list_my_groups_pagination": `{"result":{"groups":[{"title":"无 ID 群"}],"hasMore":false}}`,
|
|
}}, "--page-all")
|
|
if err != nil || payload["count"] != float64(1) {
|
|
t.Fatalf("missing-id payload=%#v err=%v", payload, err)
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageDirectMessagesAdditionalEdges(t *testing.T) {
|
|
run := func(t *testing.T, fake *larkAlignmentCaller, args ...string) (map[string]any, error) {
|
|
t.Helper()
|
|
helpers.InitDeps(fake)
|
|
root := newPlatformCoverageRoot()
|
|
var output bytes.Buffer
|
|
root.SetOut(&output)
|
|
root.SetArgs(append([]string{
|
|
"chat", "+messages-list-direct", "--user", "u1", "--time", "2026-08-05 16:49:00",
|
|
}, args...))
|
|
err := root.Execute()
|
|
if output.Len() == 0 {
|
|
return nil, err
|
|
}
|
|
var payload map[string]any
|
|
if decodeErr := json.Unmarshal(output.Bytes(), &payload); decodeErr != nil {
|
|
t.Fatal(decodeErr)
|
|
}
|
|
return payload, err
|
|
}
|
|
|
|
for _, args := range [][]string{
|
|
{"chat", "+messages-list-direct", "--time", "2026-01-01"},
|
|
{"chat", "+messages-list-direct", "--user", "u1", "--time", "2026-01-01", "--limit", "0"},
|
|
{"chat", "+messages-list-direct", "--user", "u1", "--time", "2026-01-01", "--size", "0"},
|
|
} {
|
|
helpers.InitDeps(&larkAlignmentCaller{})
|
|
root := newPlatformCoverageRoot()
|
|
root.SetArgs(args)
|
|
if err := root.Execute(); err == nil {
|
|
t.Fatalf("invalid args succeeded: %v", args)
|
|
}
|
|
}
|
|
|
|
t.Run("single page terminal and continuing", func(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
response string
|
|
stop string
|
|
}{
|
|
{name: "terminal", response: `{"result":{"messages":[],"hasMore":false}}`, stop: "source_complete"},
|
|
{name: "continuing", response: `{"result":{"messages":[{"openMessageId":"m1"}],"hasMore":true,"nextCursor":1785919699136}}`, stop: "single_page"},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
payload, err := run(t, &larkAlignmentCaller{responses: map[string]string{"chat/list_individual_chat_message": tc.response}})
|
|
if err != nil || payload["stopReason"] != tc.stop {
|
|
t.Fatalf("payload=%#v err=%v", payload, err)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("read failures", func(t *testing.T) {
|
|
_, err := run(t, &larkAlignmentCaller{failProductToolAt: map[string]int{"chat/list_individual_chat_message": 1}})
|
|
if err == nil {
|
|
t.Fatal("single-page read failure unexpectedly succeeded")
|
|
}
|
|
_, err = run(t, &larkAlignmentCaller{failProductToolAt: map[string]int{"chat/list_individual_chat_message": 1}}, "--page-all")
|
|
if err == nil {
|
|
t.Fatal("first all-page read failure unexpectedly succeeded")
|
|
}
|
|
payload, err := run(t, &larkAlignmentCaller{
|
|
sequenceResponses: map[string][]string{
|
|
"chat/list_individual_chat_message": {`{"result":{"messages":[{"openMessageId":"m1"}],"hasMore":true,"nextCursor":1785919699136}}`},
|
|
},
|
|
failProductToolAt: map[string]int{"chat/list_individual_chat_message": 2},
|
|
}, "--page-all")
|
|
if err == nil || payload["partial"] != true || payload["stopReason"] != "read_failure" {
|
|
t.Fatalf("payload=%#v err=%v", payload, err)
|
|
}
|
|
})
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
responses []string
|
|
}{
|
|
{name: "unknown pagination", responses: []string{`{"result":{"messages":[{"openMessageId":"m1"}]}}`}},
|
|
{name: "empty continuing page", responses: []string{`{"result":{"messages":[],"hasMore":true,"nextCursor":1785919699136}}`}},
|
|
{name: "stalled cursor and duplicate", responses: []string{
|
|
`{"result":{"messages":[{"openMessageId":"m1"}],"hasMore":true,"nextCursor":1785919699136}}`,
|
|
`{"result":{"messages":[{"openMessageId":"m1"}],"hasMore":true,"nextCursor":1785919699136}}`,
|
|
}},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
payload, err := run(t, &larkAlignmentCaller{sequenceResponses: map[string][]string{
|
|
"chat/list_individual_chat_message": tc.responses,
|
|
}}, "--page-all")
|
|
if err == nil || payload["failedCount"] != float64(1) || payload["stopReason"] != "pagination_error" {
|
|
t.Fatalf("payload=%#v err=%v", payload, err)
|
|
}
|
|
})
|
|
}
|
|
|
|
t.Run("all-page output errors propagate", func(t *testing.T) {
|
|
fake := &larkAlignmentCaller{responses: map[string]string{
|
|
"chat/list_individual_chat_message": `{"result":{"messages":[],"hasMore":false}}`,
|
|
}}
|
|
helpers.InitDeps(fake)
|
|
root := newPlatformCoverageRoot()
|
|
root.SetOut(chatOutputErrorWriter{err: errors.New("fixture output")})
|
|
root.SetArgs([]string{"chat", "+messages-list-direct", "--user", "u1", "--time", "2026-01-01", "--page-all"})
|
|
if err := root.Execute(); err == nil {
|
|
t.Fatal("output error was swallowed")
|
|
}
|
|
})
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
value any
|
|
ok bool
|
|
}{
|
|
{name: "int", value: int(1), ok: true},
|
|
{name: "int32", value: int32(2), ok: true},
|
|
{name: "float32", value: float32(3), ok: true},
|
|
{name: "float64", value: float64(4), ok: true},
|
|
{name: "string", value: "5", ok: true},
|
|
{name: "fractional float32", value: float32(1.5)},
|
|
{name: "negative float64", value: float64(-1)},
|
|
{name: "invalid string", value: "invalid"},
|
|
{name: "unsupported", value: true},
|
|
{name: "zero", value: int64(0)},
|
|
} {
|
|
t.Run("cursor "+tc.name, func(t *testing.T) {
|
|
key, boundary, err := directMessageCursorBoundary(tc.value)
|
|
if tc.ok {
|
|
if err != nil || key == "" || boundary == "" {
|
|
t.Fatalf("cursor=(%q,%q,%v)", key, boundary, err)
|
|
}
|
|
return
|
|
}
|
|
if err == nil {
|
|
t.Fatalf("invalid cursor succeeded: (%q,%q)", key, boundary)
|
|
}
|
|
})
|
|
}
|
|
}
|