refactor(search): address PR #750 round 2 review feedback
Implements fire-and-forget eviction (ADR-019 §"Lazy eviction"): the search response no longer waits on Qdrant deletes, instead spawning evict() on a long-lived lifespan-owned task group. Falls back to inline eviction in modes without vector sync and in unit tests. Also: harden _verify_news_items against non-numeric ids (fail open instead of crashing the verifier); document the get_file_info None-on-404 contract; add INDEXED_DOC_TYPES single source of truth in vector/scanner.py referenced by the CI-guard test; write a Verify-on-Read Latency Budget section in docs/configuration.md covering the unbounded news.get_items fetch. Closes the two remaining ADR-019 implementation checklist items. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
7784ec02d7
commit
21e5608a39
@@ -17,6 +17,7 @@ from nextcloud_mcp_server.search.verification import (
|
||||
get_supported_doc_types,
|
||||
verify_search_results,
|
||||
)
|
||||
from nextcloud_mcp_server.vector.scanner import INDEXED_DOC_TYPES
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
@@ -58,13 +59,13 @@ def _http_error(status_code: int) -> HTTPStatusError:
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_supported_doc_types_covers_indexed_types():
|
||||
"""ADR-019 implementation checklist: every indexed doc_type has a verifier.
|
||||
"""ADR-019 CI guard: every doc_type indexed by the scanner has a verifier.
|
||||
|
||||
Indexed types are defined in vector/scanner.py and vector/processor.py:
|
||||
note, file, deck_card, news_item.
|
||||
`INDEXED_DOC_TYPES` is the single source of truth in `vector/scanner.py`;
|
||||
this test fails if a new indexed type is added without a registered
|
||||
verifier in `search/verification.py`.
|
||||
"""
|
||||
expected = {"note", "file", "deck_card", "news_item"}
|
||||
assert get_supported_doc_types() >= expected
|
||||
assert get_supported_doc_types() >= INDEXED_DOC_TYPES
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -423,6 +424,7 @@ async def test_verify_search_results_dedupes_chunks_per_document(mocker):
|
||||
|
||||
@pytest.mark.unit
|
||||
async def test_verify_search_results_drops_inaccessible_and_evicts(mocker):
|
||||
"""Inline-fallback path (no eviction_task_group): evict completes before return."""
|
||||
spy_evict = mocker.AsyncMock()
|
||||
mocker.patch.object(verification, "delete_document_points", spy_evict)
|
||||
|
||||
@@ -442,6 +444,49 @@ async def test_verify_search_results_drops_inaccessible_and_evicts(mocker):
|
||||
spy_evict.assert_awaited_once_with(99, "note", "alice")
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
async def test_verify_search_results_fire_and_forget_eviction(mocker):
|
||||
"""When eviction_task_group is provided, eviction does not block the response.
|
||||
|
||||
Validates the ADR-019 design: spawn evict() on the lifespan-owned task
|
||||
group via start_soon so the search response returns immediately. The
|
||||
eviction still runs (verified after the task group exits).
|
||||
"""
|
||||
eviction_started = anyio.Event()
|
||||
eviction_may_complete = anyio.Event()
|
||||
eviction_completed = anyio.Event()
|
||||
|
||||
async def slow_delete(doc_id, doc_type, user_id):
|
||||
eviction_started.set()
|
||||
await eviction_may_complete.wait()
|
||||
eviction_completed.set()
|
||||
|
||||
mocker.patch.object(
|
||||
verification,
|
||||
"delete_document_points",
|
||||
mocker.AsyncMock(side_effect=slow_delete),
|
||||
)
|
||||
|
||||
note_verifier = mocker.AsyncMock(return_value=set()) # both inaccessible
|
||||
mocker.patch.dict(verification._VERIFIERS, {"note": note_verifier}, clear=False)
|
||||
|
||||
results = [_make_result(99, doc_type="note")]
|
||||
client = SimpleNamespace(username="alice")
|
||||
|
||||
async with anyio.create_task_group() as tg:
|
||||
kept = await verify_search_results(client, results, eviction_task_group=tg)
|
||||
# 1. Search response was returned …
|
||||
assert kept == []
|
||||
# 2. … even though eviction has started but not finished.
|
||||
await eviction_started.wait()
|
||||
assert not eviction_completed.is_set()
|
||||
# 3. Now allow eviction to complete; the task group exit awaits it.
|
||||
eviction_may_complete.set()
|
||||
|
||||
# After the task group exits, the eviction must have run.
|
||||
assert eviction_completed.is_set()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
async def test_verify_search_results_no_eviction_when_disabled(mocker):
|
||||
spy_evict = mocker.AsyncMock()
|
||||
|
||||
Reference in New Issue
Block a user