Pagination deploy fix

This commit is contained in:
2026-05-24 09:25:11 +02:00
parent 6ac18e36c8
commit 58b8955e07
2 changed files with 75 additions and 11 deletions
+34 -1
View File
@@ -1439,6 +1439,7 @@ func (c *nodeBBClient) listNamespacePagesWithQuery(cid int, query string) ([]nod
var out []nodeBBWikiPage var out []nodeBBWikiPage
after := "" after := ""
seenCursors := map[string]struct{}{} seenCursors := map[string]struct{}{}
seenPages := map[string]struct{}{}
for { for {
path := fmt.Sprintf("/api/v3/plugins/westgate-wiki/namespace/%d/pages?limit=80", cid) path := fmt.Sprintf("/api/v3/plugins/westgate-wiki/namespace/%d/pages?limit=80", cid)
if query != "" { if query != "" {
@@ -1452,14 +1453,34 @@ func (c *nodeBBClient) listNamespacePagesWithQuery(cid int, query string) ([]nod
return nil, err return nil, err
} }
pages, next, hasMore := parseNodeBBWikiPageList(doc) pages, next, hasMore := parseNodeBBWikiPageList(doc)
out = append(out, pages...) newPages := 0
for _, page := range pages {
identity := nodeBBWikiPageIdentity(page)
if identity == "" {
out = append(out, page)
newPages++
continue
}
if _, ok := seenPages[identity]; ok {
continue
}
seenPages[identity] = struct{}{}
out = append(out, page)
newPages++
}
if !hasMore || next == "" { if !hasMore || next == "" {
return out, nil return out, nil
} }
if after != "" && next == after { if after != "" && next == after {
if newPages == 0 {
return out, nil
}
return nil, fmt.Errorf("NodeBB wiki namespace %d returned repeated pagination cursor %q", cid, next) return nil, fmt.Errorf("NodeBB wiki namespace %d returned repeated pagination cursor %q", cid, next)
} }
if _, ok := seenCursors[next]; ok { if _, ok := seenCursors[next]; ok {
if newPages == 0 {
return out, nil
}
return nil, fmt.Errorf("NodeBB wiki namespace %d returned repeated pagination cursor %q", cid, next) return nil, fmt.Errorf("NodeBB wiki namespace %d returned repeated pagination cursor %q", cid, next)
} }
seenCursors[next] = struct{}{} seenCursors[next] = struct{}{}
@@ -1467,6 +1488,18 @@ func (c *nodeBBClient) listNamespacePagesWithQuery(cid int, query string) ([]nod
} }
} }
func nodeBBWikiPageIdentity(page nodeBBWikiPage) string {
if page.TID != 0 {
return fmt.Sprintf("tid:%d", page.TID)
}
for _, value := range []string{page.WikiPath, page.CanonicalPath, page.Slug, page.Title} {
if trimmed := strings.TrimSpace(value); trimmed != "" {
return strings.ToLower(trimmed)
}
}
return ""
}
func (c *nodeBBClient) createTopic(cid int, title, content, summary string) (nodeBBPost, error) { func (c *nodeBBClient) createTopic(cid int, title, content, summary string) (nodeBBPost, error) {
body := map[string]any{"cid": cid, "title": title, "content": content, "sourceContent": content} body := map[string]any{"cid": cid, "title": title, "content": content, "sourceContent": content}
if summary != "" { if summary != "" {
+41 -10
View File
@@ -226,18 +226,52 @@ func TestDeployWikiReportsLiveExecutionProgress(t *testing.T) {
} }
} }
func TestNodeBBNamespacePaginationRejectsRepeatedCursor(t *testing.T) { func TestNodeBBNamespacePaginationStopsWhenRepeatedCursorReturnsNoNewPages(t *testing.T) {
requests := 0 requests := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requests++ requests++
if requests > 2 {
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{ _ = json.NewEncoder(w).Encode(map[string]any{
"response": map[string]any{ "response": map[string]any{
"pages": []map[string]any{}, "pages": []map[string]any{
{"tid": 2461, "title": "Favored Enemy: Elves", "titleLeaf": "Favored Enemy: Elves"},
},
"hasMore": true,
"nextCursor": "same-cursor",
},
})
}))
defer server.Close()
client := newNodeBBClient(server.URL, "nodebb-token")
pages, err := client.listNamespacePages(9)
if err != nil {
t.Fatalf("expected repeated cursor with duplicate pages to stop cleanly, got %v", err)
}
if len(pages) != 1 || pages[0].TID != 2461 {
t.Fatalf("expected one unique page from repeated cursor response, got %#v", pages)
}
if requests != 2 {
t.Fatalf("expected pagination to stop after repeated cursor, got %d requests", requests)
}
}
func TestNodeBBNamespacePaginationRejectsRepeatedCursorWithNewPages(t *testing.T) {
requests := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requests++
w.Header().Set("Content-Type", "application/json")
pages := []map[string]any{
{"tid": 1, "title": "First", "titleLeaf": "First"},
}
if requests == 2 {
pages = []map[string]any{
{"tid": 2, "title": "Second", "titleLeaf": "Second"},
}
}
_ = json.NewEncoder(w).Encode(map[string]any{
"response": map[string]any{
"pages": pages,
"hasMore": true, "hasMore": true,
"nextCursor": "same-cursor", "nextCursor": "same-cursor",
}, },
@@ -248,14 +282,11 @@ func TestNodeBBNamespacePaginationRejectsRepeatedCursor(t *testing.T) {
client := newNodeBBClient(server.URL, "nodebb-token") client := newNodeBBClient(server.URL, "nodebb-token")
_, err := client.listNamespacePages(9) _, err := client.listNamespacePages(9)
if err == nil { if err == nil {
t.Fatal("expected repeated cursor error") t.Fatal("expected repeated cursor with new pages to fail")
} }
if !strings.Contains(err.Error(), "repeated pagination cursor") { if !strings.Contains(err.Error(), "repeated pagination cursor") {
t.Fatalf("expected repeated cursor error, got %v", err) t.Fatalf("expected repeated cursor error, got %v", err)
} }
if requests != 2 {
t.Fatalf("expected pagination to stop after repeated cursor, got %d requests", requests)
}
} }
func TestCollectLocalPagesReadsGeneratedPageIDFromMarkerWhenPathIsTitleBased(t *testing.T) { func TestCollectLocalPagesReadsGeneratedPageIDFromMarkerWhenPathIsTitleBased(t *testing.T) {