internal/mcp: add pagination for resources
Change-Id: I9f257fa09fe82e53087616859d1ad0c03f57dab6 Reviewed-on: https://go-review.googlesource.com/c/tools/+/675696 Reviewed-by: Jonathan Amsterdam <jba@google.com> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Robert Findley <rfindley@google.com>
This commit is contained in:
parent
df26370d22
commit
dbbdf5a181
1 changed files with 278 additions and 166 deletions
202
mcp/server_test.go
Normal file
202
mcp/server_test.go
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
// Copyright 2025 The Go MCP SDK Authors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"log"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
type TestItem struct {
|
||||
Name string
|
||||
Value string
|
||||
}
|
||||
|
||||
var allTestItems = []*TestItem{
|
||||
{"alpha", "val-A"},
|
||||
{"bravo", "val-B"},
|
||||
{"charlie", "val-C"},
|
||||
{"delta", "val-D"},
|
||||
{"echo", "val-E"},
|
||||
{"foxtrot", "val-F"},
|
||||
{"golf", "val-G"},
|
||||
{"hotel", "val-H"},
|
||||
{"india", "val-I"},
|
||||
{"juliet", "val-J"},
|
||||
{"kilo", "val-K"},
|
||||
}
|
||||
|
||||
// getCursor encodes a string input into a URL-safe base64 cursor,
|
||||
// fatally logging any encoding errors.
|
||||
func getCursor(input string) string {
|
||||
cursor, err := encodeCursor(input)
|
||||
if err != nil {
|
||||
log.Fatalf("encodeCursor(%s) error = %v", input, err)
|
||||
}
|
||||
return cursor
|
||||
}
|
||||
|
||||
func TestPaginateBasic(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
initialItems []*TestItem
|
||||
inputCursor string
|
||||
inputPageSize int
|
||||
wantFeatures []*TestItem
|
||||
wantNextCursor string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "FirstPage_DefaultSize_Full",
|
||||
initialItems: allTestItems,
|
||||
inputCursor: "",
|
||||
inputPageSize: 5,
|
||||
wantFeatures: allTestItems[0:5],
|
||||
wantNextCursor: getCursor("echo"), // Based on last item of first page
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "SecondPage_DefaultSize_Full",
|
||||
initialItems: allTestItems,
|
||||
inputCursor: getCursor("echo"),
|
||||
inputPageSize: 5,
|
||||
wantFeatures: allTestItems[5:10],
|
||||
wantNextCursor: getCursor("juliet"), // Based on last item of second page
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "SecondPage_DefaultSize_Full_OutOfOrder",
|
||||
initialItems: append(allTestItems[5:], allTestItems[0:5]...),
|
||||
inputCursor: getCursor("echo"),
|
||||
inputPageSize: 5,
|
||||
wantFeatures: allTestItems[5:10],
|
||||
wantNextCursor: getCursor("juliet"), // Based on last item of second page
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "SecondPage_DefaultSize_Full_Duplicates",
|
||||
initialItems: append(allTestItems, allTestItems[0:5]...),
|
||||
inputCursor: getCursor("echo"),
|
||||
inputPageSize: 5,
|
||||
wantFeatures: allTestItems[5:10],
|
||||
wantNextCursor: getCursor("juliet"), // Based on last item of second page
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "LastPage_Remaining",
|
||||
initialItems: allTestItems,
|
||||
inputCursor: getCursor("juliet"),
|
||||
inputPageSize: 5,
|
||||
wantFeatures: allTestItems[10:11], // Only 1 item left
|
||||
wantNextCursor: "", // No more pages
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "PageSize_1",
|
||||
initialItems: allTestItems,
|
||||
inputCursor: "",
|
||||
inputPageSize: 1,
|
||||
wantFeatures: allTestItems[0:1],
|
||||
wantNextCursor: getCursor("alpha"),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "PageSize_All",
|
||||
initialItems: allTestItems,
|
||||
inputCursor: "",
|
||||
inputPageSize: len(allTestItems), // Page size equals total
|
||||
wantFeatures: allTestItems,
|
||||
wantNextCursor: "", // No more pages
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "PageSize_LargerThanAll",
|
||||
initialItems: allTestItems,
|
||||
inputCursor: "",
|
||||
inputPageSize: len(allTestItems) + 5, // Page size larger than total
|
||||
wantFeatures: allTestItems,
|
||||
wantNextCursor: "",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "EmptySet",
|
||||
initialItems: nil,
|
||||
inputCursor: "",
|
||||
inputPageSize: 5,
|
||||
wantFeatures: nil,
|
||||
wantNextCursor: "",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "InvalidCursor",
|
||||
initialItems: allTestItems,
|
||||
inputCursor: "not-a-valid-gob-base64-cursor",
|
||||
inputPageSize: 5,
|
||||
wantFeatures: nil, // Should be nil for error cases
|
||||
wantNextCursor: "",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "AboveNonExistentID",
|
||||
initialItems: allTestItems,
|
||||
inputCursor: getCursor("dne"), // A UID that doesn't exist
|
||||
inputPageSize: 5,
|
||||
wantFeatures: allTestItems[4:9], // Should return elements above UID.
|
||||
wantNextCursor: getCursor("india"),
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
fs := newFeatureSet(func(t *TestItem) string { return t.Name })
|
||||
fs.add(tc.initialItems...)
|
||||
gotFeatures, gotNextCursor, err := paginateList(fs, tc.inputCursor, tc.inputPageSize)
|
||||
if (err != nil) != tc.wantErr {
|
||||
t.Errorf("paginateList(%s) error, got %v, wantErr %v", tc.name, err, tc.wantErr)
|
||||
}
|
||||
if diff := cmp.Diff(tc.wantFeatures, gotFeatures); diff != "" {
|
||||
t.Errorf("paginateList(%s) mismatch (-want +got):\n%s", tc.name, diff)
|
||||
}
|
||||
if tc.wantNextCursor != gotNextCursor {
|
||||
t.Errorf("paginateList(%s) nextCursor, got %v, want %v", tc.name, gotNextCursor, tc.wantNextCursor)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaginateVariousPageSizes(t *testing.T) {
|
||||
fs := newFeatureSet(func(t *TestItem) string { return t.Name })
|
||||
fs.add(allTestItems...)
|
||||
// Try all possible page sizes, ensuring we get the correct list of items.
|
||||
for pageSize := 1; pageSize < len(allTestItems)+1; pageSize++ {
|
||||
var gotItems []*TestItem
|
||||
var nextCursor string
|
||||
wantChunks := slices.Collect(slices.Chunk(allTestItems, pageSize))
|
||||
index := 0
|
||||
// Iterate through all pages, comparing sub-slices to the paginated list.
|
||||
for {
|
||||
gotFeatures, gotNextCursor, err := paginateList(fs, nextCursor, pageSize)
|
||||
if err != nil {
|
||||
}
|
||||
if diff := cmp.Diff(wantChunks[index], gotFeatures); diff != "" {
|
||||
t.Errorf("paginateList mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
gotItems = append(gotItems, gotFeatures...)
|
||||
nextCursor = gotNextCursor
|
||||
if nextCursor == "" {
|
||||
break
|
||||
}
|
||||
index++
|
||||
}
|
||||
|
||||
if len(gotItems) != len(allTestItems) {
|
||||
t.Fatalf("paginateList() returned %d items, want %d", len(allTestItems), len(gotItems))
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue