Files
library-desk/tests/test_wikijs_pagination.py
T
jpmschweitzerandClaude Fable 5 0e57be75be fix: normalize path prefix in wiki search so tenant filtering matches
get_wikijs_namespace() returns '/users/{user}' with a leading slash while
Wiki.js search results carry paths without one, so the prefix filter in
search_pages rejected every result - wiki search returned empty for every
tenant. Found by cross-repo integration verification; the librarian now
gets real search results. Compare slash-normalized on both sides.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 15:33:00 +02:00

148 lines
5.5 KiB
Python

"""
Unit tests for WikiJSClient page listing and pagination (offline).
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
from unittest.mock import AsyncMock
from src.clients.wikijs_client import WikiJSClient
def make_pages(count_other: int, count_user: int):
"""Build a fake TITLE-ordered page listing: 'other/' pages sort before 'users/jp/' pages."""
pages = [
{"id": i, "path": f"other/p{i:03d}", "title": f"A{i:03d}", "tags": []}
for i in range(count_other)
]
pages += [
{"id": count_other + i, "path": f"users/jp/p{i:03d}", "title": f"Z{i:03d}", "tags": ["projects"]}
for i in range(count_user)
]
return pages
@pytest.fixture
def client():
"""WikiJSClient with a mocked GraphQL layer serving 138 pages (100 other + 38 user)."""
client = WikiJSClient("http://wiki.test", "")
client._all_pages = make_pages(100, 38)
client._requested_limits = []
async def fake_execute(query, variables=None):
limit = variables["limit"]
client._requested_limits.append(limit)
return {"pages": {"list": client._all_pages[:limit]}}
client._execute_query = fake_execute
return client
@pytest.mark.unit
class TestListAllPagesPagination:
"""list_all_pages must exhaust the listing via a limit-growth loop."""
async def test_grows_limit_until_count_stabilizes(self, client):
pages = await client.list_all_pages(batch_size=50)
# 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_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, 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):
"""All 38 user pages are returned even though they sort last (beyond batch_size)."""
pages = await client.list_all_pages(path_prefix="users/jp", batch_size=50)
assert len(pages) == 38
assert all(p["path"].startswith("users/jp") for p in pages)
async def test_empty_wiki(self, client):
client._all_pages = []
pages = await client.list_all_pages(batch_size=100)
assert pages == []
@pytest.mark.unit
class TestListPagesLimitAfterFilter:
"""list_pages must apply `limit` AFTER client-side filters, not before."""
async def test_prefix_filter_with_small_limit(self, client):
"""Old defect: API limit=5 returned 5 'other/' pages, filter dropped all -> 0 results."""
pages = await client.list_pages(path_prefix="users/jp", limit=5)
assert len(pages) == 5
assert all(p["path"].startswith("users/jp") for p in pages)
async def test_tag_filter_with_small_limit(self, client):
pages = await client.list_pages(tags=["projects"], limit=10)
assert len(pages) == 10
assert all("projects" in p["tags"] for p in pages)
async def test_unfiltered_passes_limit_to_api(self, client):
pages = await client.list_pages(limit=7)
assert client._requested_limits == [7]
assert len(pages) == 7
async def test_tags_normalized_to_list(self, client):
client._all_pages = [{"id": 1, "path": "home", "title": "Home", "tags": None}]
pages = await client.list_pages(limit=10)
assert pages[0]["tags"] == []
class TestSearchPagesPrefixNormalization:
"""search_pages must match Wiki.js paths (no leading slash) against
get_wikijs_namespace prefixes (leading slash)."""
@pytest.mark.asyncio
async def test_slashed_prefix_matches_unslashed_paths(self):
client = WikiJSClient("http://wiki.test", "k")
client._execute_query = AsyncMock(return_value={
"pages": {"search": {"results": [
{"id": "399", "path": "users/llm_tester/docker-guide",
"title": "Docker Guide", "description": ""},
{"id": "1", "path": "users/jpmschweitzer/other",
"title": "Other", "description": ""},
]}}
})
results = await client.search_pages("docker", path_prefix="/users/llm_tester")
assert [r["id"] for r in results] == ["399"]