fix(webdav): await fallback, guard dedup key, split paging for complexity

Address review on #849:

- Critical: add missing `await` on the exception-path fallback in
  search_files_all -- it returned a coroutine instead of the result list.
  Add unit tests for both the offset-page-raises (fallback) and
  offset-zero-raises (propagate) paths, which previously had no coverage.
- Guard `_key` dedup against items missing both file_id and path (fall back
  to id(item)) so they can't collapse under a shared None key and drop rows.
- Document the offset-ignored discard-and-refetch decision.
- Split the offset paging into `_search_offset_paged` (returns None to signal
  fallback) and share the truncation warning via `_warn_if_truncated`,
  cutting cognitive complexity below the threshold (SonarCloud S3776).
- Make the test side_effect helpers synchronous (SonarCloud S7503).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Chris Coutinho
2026-06-04 13:44:37 +02:00
co-authored by Claude Opus 4.8
parent 01cb7cf08c
commit eaa898e6eb
2 changed files with 84 additions and 29 deletions
+35 -4
View File
@@ -52,7 +52,7 @@ async def test_offset_honored_pages_through_entire_corpus(mocker):
client = _make_client(mocker)
corpus = _corpus(250)
async def fake(*, limit, offset=0, **_):
def fake(*, limit, offset=0, **_):
return corpus[offset : offset + limit]
client.search_files = AsyncMock(side_effect=fake)
@@ -69,7 +69,7 @@ async def test_offset_ignored_falls_back_to_single_fetch(mocker):
client = _make_client(mocker)
corpus = _corpus(250)
async def fake(*, limit, offset=0, **_):
def fake(*, limit, offset=0, **_):
# offset IGNORED: always return the first ``limit`` rows.
return corpus[:limit]
@@ -87,7 +87,7 @@ async def test_truncation_warns_and_increments_metric(mocker):
client = _make_client(mocker)
corpus = _corpus(20)
async def fake(*, limit, offset=0, **_):
def fake(*, limit, offset=0, **_):
return corpus[offset : offset + limit]
client.search_files = AsyncMock(side_effect=fake)
@@ -106,7 +106,7 @@ async def test_offset_ignored_fallback_truncation_metric(mocker):
client = _make_client(mocker)
corpus = _corpus(40)
async def fake(*, limit, offset=0, **_):
def fake(*, limit, offset=0, **_):
return corpus[:limit] # offset ignored
client.search_files = AsyncMock(side_effect=fake)
@@ -120,6 +120,37 @@ async def test_offset_ignored_fallback_truncation_metric(mocker):
metric.inc.assert_called_once()
async def test_offset_page_exception_falls_back_to_single_fetch(mocker):
"""If an offset page *raises* (e.g. server rejects firstresult), the
exception fallback must still return the full corpus -- and it must be
awaited (regression guard for a missing ``await``)."""
client = _make_client(mocker)
corpus = _corpus(80)
def fake(*, limit, offset=0, **_):
if offset > 0:
raise RuntimeError("server rejected <d:firstresult>")
return corpus[:limit]
client.search_files = AsyncMock(side_effect=fake)
results = await client.search_files_all(scope="dir", page_size=50)
# page0 (0..49) fills; page1(offset=50) raises -> fallback single fetch
# returns the whole corpus. A non-awaited coroutine would fail these.
assert isinstance(results, list)
assert [r["file_id"] for r in results] == list(range(80))
async def test_offset_page_exception_at_offset_zero_propagates(mocker):
"""A failure on the very first page is a real error, not a paging quirk."""
client = _make_client(mocker)
client.search_files = AsyncMock(side_effect=RuntimeError("boom"))
with pytest.raises(RuntimeError, match="boom"):
await client.search_files_all(scope="dir", page_size=50)
async def test_find_all_by_type_delegates_to_search_files_all(mocker):
client = _make_client(mocker)
client.search_files_all = AsyncMock(return_value=_corpus(3))