refactor(search): address PR #750 round 5 review feedback
Tightens verifier consistency, closes test gaps, hardens the fire-and-forget eviction snapshot, and routes the new concurrency knob through Settings. - Pre-flight ``int()`` guard in ``_verify_notes`` mirrors ``_verify_deck_cards``, so a non-numeric note id produces a type-specific log line instead of falling through to the generic "unexpected error" branch. - Adds explicit 403 tests for the file and news verifiers (symmetry with the existing notes/deck 403 tests) plus a ``non_numeric_id_keeps`` test. - ``AppContext`` and ``OAuthAppContext`` no longer snapshot ``_vector_sync_state.eviction_task_group`` at lifespan-yield time. Both expose it as a ``@property`` that reads the singleton dynamically, removing the order-sensitive race where a future startup-ordering change could silently degrade fire-and-forget eviction to inline forever. - Adds ``verification_concurrency`` (env var ``VERIFICATION_CONCURRENCY``, default 20) to ``Settings`` with a dynaconf validator; ``verify_search_results`` resolves the cap lazily from settings when the caller doesn't override it. - Enriches the news verifier TODO to call out that ``batch_size=-1`` is intentional — a numeric ceiling would silently break correctness because any item beyond the cap would be missing from ``present_ids`` and dropped. - Updates ``Optional[TaskGroup]`` to ``TaskGroup | None`` per project style. 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
926722b09d
commit
ffcca23a7b
@@ -137,6 +137,24 @@ async def test_verify_notes_unexpected_exception_keeps(mocker):
|
||||
assert result == {7}
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
async def test_verify_notes_non_numeric_id_keeps(mocker):
|
||||
"""Non-numeric note id must not surface as a generic 'unexpected error'.
|
||||
|
||||
The defensive int() guard runs before the network call and produces a
|
||||
type-specific log line; result is kept (fail-open).
|
||||
"""
|
||||
notes_client = SimpleNamespace(
|
||||
get_note=mocker.AsyncMock(side_effect=AssertionError("must not be called"))
|
||||
)
|
||||
client = SimpleNamespace(notes=notes_client, username="alice")
|
||||
|
||||
result = await _verify_notes(client, [_make_result("not-a-number")], _sem())
|
||||
|
||||
assert result == {"not-a-number"}
|
||||
notes_client.get_note.assert_not_awaited()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
async def test_verify_notes_mixed_outcomes(mocker):
|
||||
"""Mix of accessible, deleted, and transient — only deleted is dropped."""
|
||||
@@ -207,6 +225,27 @@ async def test_verify_news_items_api_404_drops_all(mocker):
|
||||
assert result == set()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
async def test_verify_news_items_api_403_drops_all(mocker):
|
||||
"""News API 403 (e.g. user lost access to the app) drops all items."""
|
||||
news_client = SimpleNamespace(
|
||||
get_items=mocker.AsyncMock(side_effect=_http_error(403))
|
||||
)
|
||||
client = SimpleNamespace(news=news_client, username="alice")
|
||||
|
||||
result = await _verify_news_items(
|
||||
client,
|
||||
[
|
||||
_make_result(1, doc_type="news_item"),
|
||||
_make_result(2, doc_type="news_item"),
|
||||
_make_result(3, doc_type="news_item"),
|
||||
],
|
||||
_sem(),
|
||||
)
|
||||
|
||||
assert result == set()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
async def test_verify_news_items_transient_keeps_all(mocker):
|
||||
news_client = SimpleNamespace(
|
||||
@@ -265,6 +304,23 @@ async def test_verify_files_404_via_get_file_info_drops(mocker):
|
||||
assert result == set()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
async def test_verify_files_403_drops(mocker):
|
||||
"""get_file_info raising HTTPStatusError(403) is a definitive drop."""
|
||||
webdav_client = SimpleNamespace(
|
||||
get_file_info=mocker.AsyncMock(side_effect=_http_error(403))
|
||||
)
|
||||
client = SimpleNamespace(webdav=webdav_client, username="alice")
|
||||
|
||||
result = await _verify_files(
|
||||
client,
|
||||
[_make_result(124, doc_type="file", metadata={"path": "forbidden.txt"})],
|
||||
_sem(),
|
||||
)
|
||||
|
||||
assert result == set()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
async def test_verify_files_missing_path_metadata_keeps_unverified(mocker):
|
||||
"""Without a path in metadata we cannot verify — fail open, don't drop."""
|
||||
|
||||
Reference in New Issue
Block a user