fix(doc): distinguish identical pagination pages
This commit is contained in:
+46
-5
@@ -755,7 +755,7 @@ func readAllDocBlocksForVerification(ctx context.Context, nodeID string) ([]any,
|
||||
const pageSize = 50
|
||||
const maxItems = 5000
|
||||
all := make([]any, 0, pageSize)
|
||||
seen := map[string]bool{}
|
||||
seenPageIdentities := map[string]bool{}
|
||||
for start := 0; start < maxItems; start += pageSize {
|
||||
text, err := callMCPToolReturnTextOnServer(ctx, "doc", "list_document_blocks", map[string]any{
|
||||
"nodeId": nodeID, "format": "jsonml", "startIndex": start, "endIndex": start + pageSize - 1,
|
||||
@@ -772,12 +772,13 @@ func readAllDocBlocksForVerification(ctx context.Context, nodeID string) ([]any,
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("list_document_blocks 回读缺少 blocks 数组")
|
||||
}
|
||||
encoded, _ := json.Marshal(blocks)
|
||||
key := string(encoded)
|
||||
if key != "[]" && seen[key] {
|
||||
pageIdentity := docBlockPageIdentity(blocks)
|
||||
if pageIdentity != "" && seenPageIdentities[pageIdentity] {
|
||||
return nil, fmt.Errorf("list_document_blocks 分页停滞")
|
||||
}
|
||||
seen[key] = true
|
||||
if pageIdentity != "" {
|
||||
seenPageIdentities[pageIdentity] = true
|
||||
}
|
||||
all = append(all, blocks...)
|
||||
hasMore, hasMoreKnown := payload["hasMore"].(bool)
|
||||
if hasMoreKnown && !hasMore {
|
||||
@@ -796,6 +797,46 @@ func readAllDocBlocksForVerification(ctx context.Context, nodeID string) ([]any,
|
||||
return nil, fmt.Errorf("文档块超过安全回读上限")
|
||||
}
|
||||
|
||||
func docBlockPageIdentity(blocks []any) string {
|
||||
if len(blocks) == 0 {
|
||||
return ""
|
||||
}
|
||||
ids := make([]string, 0, len(blocks))
|
||||
for _, value := range blocks {
|
||||
id := ""
|
||||
switch block := value.(type) {
|
||||
case map[string]any:
|
||||
id = directDocBlockIdentity(block)
|
||||
if id == "" {
|
||||
if element, ok := block["element"].(map[string]any); ok {
|
||||
id = directDocBlockIdentity(element)
|
||||
}
|
||||
}
|
||||
case []any:
|
||||
if len(block) > 1 {
|
||||
if attributes, ok := block[1].(map[string]any); ok {
|
||||
id = directDocBlockIdentity(attributes)
|
||||
}
|
||||
}
|
||||
}
|
||||
if id == "" {
|
||||
return ""
|
||||
}
|
||||
ids = append(ids, id)
|
||||
}
|
||||
encoded, _ := json.Marshal(ids)
|
||||
return string(encoded)
|
||||
}
|
||||
|
||||
func directDocBlockIdentity(block map[string]any) string {
|
||||
for _, key := range []string{"blockId", "id", "uuid", "elementId"} {
|
||||
if text, ok := block[key].(string); ok && strings.TrimSpace(text) != "" {
|
||||
return strings.TrimSpace(text)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func nestedDocMap(data map[string]any) map[string]any {
|
||||
for _, key := range []string{"result", "data"} {
|
||||
if nested, ok := data[key].(map[string]any); ok {
|
||||
|
||||
@@ -3,6 +3,7 @@ package helpers
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -318,6 +319,27 @@ func TestCrossPlatformCoverageDocMediaReadbackDefensiveEdges(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("identical adjacent pages advance by requested indexes", func(t *testing.T) {
|
||||
blocks := make([]any, 50)
|
||||
for index := range blocks {
|
||||
blocks[index] = map[string]any{"blockType": "paragraph"}
|
||||
}
|
||||
first, err := json.Marshal(map[string]any{"blocks": blocks, "hasMore": true, "totalCount": 100})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
second, err := json.Marshal(map[string]any{"blocks": blocks, "hasMore": false, "totalCount": 100})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
caller := &scriptedToolCaller{steps: []scriptedToolStep{{text: string(first)}, {text: string(second)}}}
|
||||
installScriptedCaller(t, caller)
|
||||
got, err := readAllDocBlocksForVerification(ctx, "node")
|
||||
if err != nil || len(got) != 100 || caller.calls != 2 {
|
||||
t.Fatalf("blocks=%d calls=%d err=%v, want 100 blocks from two pages", len(got), caller.calls, err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("unknown pagination short page", func(t *testing.T) {
|
||||
installScriptedCaller(t, &scriptedToolCaller{steps: []scriptedToolStep{{text: `{"blocks":[{"id":"a"}]}`}}})
|
||||
if blocks, err := readAllDocBlocksForVerification(ctx, "node"); err != nil || len(blocks) != 1 {
|
||||
|
||||
@@ -808,7 +808,7 @@ func readDocVerification(rt *shortcut.RuntimeContext, tool string, rawParams map
|
||||
|
||||
func readAllDocumentBlocks(rt *shortcut.RuntimeContext, base map[string]any) (map[string]any, error) {
|
||||
all := make([]any, 0, docBlockReadPageSize)
|
||||
seenPages := map[string]bool{}
|
||||
seenPageIdentities := map[string]bool{}
|
||||
for start := 0; start < docBlockReadMaxItems; start += docBlockReadPageSize {
|
||||
params := cloneMap(base)
|
||||
params["startIndex"] = start
|
||||
@@ -821,12 +821,13 @@ func readAllDocumentBlocks(rt *shortcut.RuntimeContext, base map[string]any) (ma
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("list_document_blocks 回读缺少 blocks 数组")
|
||||
}
|
||||
encoded, _ := json.Marshal(blocks)
|
||||
pageKey := string(encoded)
|
||||
if pageKey != "[]" && seenPages[pageKey] {
|
||||
pageIdentity := documentBlockPageIdentity(blocks)
|
||||
if pageIdentity != "" && seenPageIdentities[pageIdentity] {
|
||||
return nil, fmt.Errorf("list_document_blocks 分页停滞,无法证明回读完整")
|
||||
}
|
||||
seenPages[pageKey] = true
|
||||
if pageIdentity != "" {
|
||||
seenPageIdentities[pageIdentity] = true
|
||||
}
|
||||
all = append(all, blocks...)
|
||||
hasMore, known, _ := docPageState(page)
|
||||
if known && !hasMore {
|
||||
@@ -845,6 +846,33 @@ func readAllDocumentBlocks(rt *shortcut.RuntimeContext, base map[string]any) (ma
|
||||
return nil, fmt.Errorf("list_document_blocks 超过 %d 个块,无法在安全上限内完成回读", docBlockReadMaxItems)
|
||||
}
|
||||
|
||||
func documentBlockPageIdentity(blocks []any) string {
|
||||
if len(blocks) == 0 {
|
||||
return ""
|
||||
}
|
||||
ids := make([]string, 0, len(blocks))
|
||||
for _, value := range blocks {
|
||||
id := ""
|
||||
switch block := value.(type) {
|
||||
case map[string]any:
|
||||
id = blockIdentity(block, "")
|
||||
if id == "" {
|
||||
if element, ok := block["element"].(map[string]any); ok {
|
||||
id = blockIdentity(element, "")
|
||||
}
|
||||
}
|
||||
case []any:
|
||||
id = jsonMLBlockIdentity(block)
|
||||
}
|
||||
if id == "" {
|
||||
return ""
|
||||
}
|
||||
ids = append(ids, id)
|
||||
}
|
||||
encoded, _ := json.Marshal(ids)
|
||||
return string(encoded)
|
||||
}
|
||||
|
||||
func documentBlockEntries(value any) ([]any, bool) {
|
||||
switch typed := value.(type) {
|
||||
case map[string]any:
|
||||
|
||||
@@ -211,6 +211,30 @@ func TestCrossPlatformCoverageDocReadbackDefensiveEdges(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("identical adjacent pages advance by requested indexes", func(t *testing.T) {
|
||||
testseam.Swap(t, &docVerifyDelays, []time.Duration{})
|
||||
blocks := make([]any, docBlockReadPageSize)
|
||||
for index := range blocks {
|
||||
blocks[index] = map[string]any{"blockType": "paragraph"}
|
||||
}
|
||||
caller := &docCoverageCaller{responses: map[string][]map[string]any{"list_document_blocks": {
|
||||
{"blocks": blocks, "hasMore": true, "totalCount": 2 * docBlockReadPageSize},
|
||||
{"blocks": blocks, "hasMore": false, "totalCount": 2 * docBlockReadPageSize},
|
||||
}}}
|
||||
if err := runDocCoverage(t, Update, caller, "--node", "n", "--command", "block_delete", "--block-id", "target", "--yes"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
starts := []int{}
|
||||
for _, call := range caller.history {
|
||||
if call.tool == "list_document_blocks" {
|
||||
starts = append(starts, call.params["startIndex"].(int))
|
||||
}
|
||||
}
|
||||
if fmt.Sprint(starts) != "[0 50]" {
|
||||
t.Fatalf("pagination starts = %v, want [0 50]", starts)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("total count terminates pagination", func(t *testing.T) {
|
||||
testseam.Swap(t, &docVerifyDelays, []time.Duration{})
|
||||
caller := &docCoverageCaller{responses: map[string][]map[string]any{"list_document_blocks": {{"blocks": []any{map[string]any{"id": "other"}}, "totalCount": 1}}}}
|
||||
|
||||
Reference in New Issue
Block a user