fix: grow Wiki.js listing limit until page count stabilizes
Live evidence during quality-report verification: pages.list(limit=100) returned 43 pages while 140 existed; limit=500 returned all 140. Wiki.js applies the limit BEFORE its own visibility filtering, so a response with fewer pages than requested does NOT prove the listing is complete. The Phase A limit-growth loop stopped on len < limit and silently truncated listings (page counts, orphan cleanups, integrity scans, and the quality report all consume this listing). The loop now doubles the limit until the returned count stops increasing (fixed point), at the cost of one confirming fetch. Offline pagination tests updated, including a regression test simulating the pre-filter limit behavior. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QbFZyDvYksazX6nYQYZ67L
This commit is contained in:
@@ -46,6 +46,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- **LLM call timeouts** - Phase 0 keyword extraction and Phase 4 re-ranking are wrapped in a 12s `asyncio.wait_for` with graceful fallback, so a hung Ollama call can no longer gate retrieval for the full 120s client timeout.
|
||||
- **`/stats` wiki page count** - The endpoint passed the bare user name as path prefix (matching nothing) and always reported 0 pages; it now counts pages under `users/{user}`.
|
||||
- **Wiki.js page listing** - `list_pages` applied the API-side `limit` before client-side path/tag filters, dropping matching pages that sort late; the limit now applies after filtering. `list_all_pages` replaced its fake pagination loop with a real limit-growth loop (Wiki.js 2.x `pages.list` has no offset argument) that fetches until the API returns fewer pages than requested.
|
||||
- **Wiki.js listing completeness under pre-filter limits** - Observed live: `pages.list(limit=100)` returned 43 pages while 140 existed (`limit=500` returned all) — Wiki.js applies the limit BEFORE its own visibility filtering, so "fewer pages than requested" does not mean the listing is complete and the limit-growth loop stopped early, silently truncating listings (page counts, cleanups, integrity scans). The loop now grows the limit until the returned count stops increasing (fixed point), at the cost of one confirming fetch.
|
||||
|
||||
## [1.7.3] - 2026-01-07
|
||||
|
||||
|
||||
@@ -143,9 +143,12 @@ class WikiJSClient:
|
||||
Fetch ALL pages from the GraphQL API.
|
||||
|
||||
The Wiki.js 2.x `pages.list` query only supports a `limit` argument
|
||||
(no offset - verified via GraphQL introspection), so exhaustive
|
||||
listing works by growing the limit until the API returns fewer
|
||||
pages than requested.
|
||||
(no offset - verified via GraphQL introspection). Crucially, the
|
||||
limit is applied BEFORE Wiki.js's own visibility filtering, so a
|
||||
response with fewer pages than requested does NOT mean the listing
|
||||
is complete (observed live: limit=100 -> 43 pages, limit=500 ->
|
||||
140 pages). Exhaustive listing therefore grows the limit until the
|
||||
returned page count stops increasing.
|
||||
|
||||
Args:
|
||||
initial_limit: Page count for the first request
|
||||
@@ -155,11 +158,16 @@ class WikiJSClient:
|
||||
"""
|
||||
max_limit = 100_000 # Safety cap against pathological growth
|
||||
limit = max(initial_limit, 1)
|
||||
previous_count: Optional[int] = None
|
||||
|
||||
while True:
|
||||
pages = await self._fetch_pages(limit)
|
||||
if len(pages) < limit or limit >= max_limit:
|
||||
# Complete when a grown limit yields no new pages (fixed point)
|
||||
if previous_count is not None and len(pages) == previous_count:
|
||||
return pages
|
||||
if limit >= max_limit:
|
||||
return pages
|
||||
previous_count = len(pages)
|
||||
limit = min(limit * 2, max_limit)
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
"""
|
||||
Unit tests for WikiJSClient page listing and pagination (offline).
|
||||
|
||||
Wiki.js 2.x `pages.list` supports only a `limit` argument (no offset), so
|
||||
exhaustive listing works by growing the limit until the API returns fewer
|
||||
pages than requested. These tests mock the GraphQL layer.
|
||||
Wiki.js 2.x `pages.list` supports only a `limit` argument (no offset), and
|
||||
the limit is applied BEFORE Wiki.js's own visibility filter — fewer pages
|
||||
than requested does NOT mean the listing is complete. Exhaustive listing
|
||||
therefore grows the limit until the returned count stops increasing.
|
||||
These tests mock the GraphQL layer.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
@@ -44,17 +46,39 @@ def client():
|
||||
class TestListAllPagesPagination:
|
||||
"""list_all_pages must exhaust the listing via a limit-growth loop."""
|
||||
|
||||
async def test_grows_limit_until_exhausted(self, client):
|
||||
async def test_grows_limit_until_count_stabilizes(self, client):
|
||||
pages = await client.list_all_pages(batch_size=50)
|
||||
|
||||
# 50 -> full batch, 100 -> full batch, 200 -> 138 < 200 done
|
||||
assert client._requested_limits == [50, 100, 200]
|
||||
# 50 -> 50, 100 -> 100, 200 -> 138, 400 -> 138 (stable) done
|
||||
assert client._requested_limits == [50, 100, 200, 400]
|
||||
assert len(pages) == 138
|
||||
|
||||
async def test_single_call_when_first_batch_not_full(self, client):
|
||||
async def test_confirms_completeness_with_second_fetch(self, client):
|
||||
"""A single not-full batch is NOT trusted (limit precedes Wiki.js's
|
||||
visibility filter); a confirming fetch at a doubled limit runs."""
|
||||
pages = await client.list_all_pages(batch_size=500)
|
||||
|
||||
assert client._requested_limits == [500]
|
||||
assert client._requested_limits == [500, 1000]
|
||||
assert len(pages) == 138
|
||||
|
||||
async def test_pre_filter_limit_does_not_truncate(self, client):
|
||||
"""Regression (observed live): Wiki.js applies `limit` before its
|
||||
visibility filter, so limit=100 returned 43 pages while 140 existed.
|
||||
The old `len < limit` stop condition silently dropped pages."""
|
||||
all_pages = client._all_pages
|
||||
|
||||
async def fake_execute(query, variables=None):
|
||||
limit = variables["limit"]
|
||||
client._requested_limits.append(limit)
|
||||
# Only ~half the pages within the limit window are visible
|
||||
return {"pages": {"list": all_pages[: limit // 2]}}
|
||||
|
||||
client._execute_query = fake_execute
|
||||
|
||||
pages = await client.list_all_pages(batch_size=100)
|
||||
|
||||
# 100 -> 50 (< limit, but NOT complete), 200 -> 100, 400 -> 138,
|
||||
# 800 -> 138 (stable) done
|
||||
assert len(pages) == 138
|
||||
|
||||
async def test_path_prefix_filter_after_exhaustion(self, client):
|
||||
|
||||
Reference in New Issue
Block a user