test(integration): address round-1 review — unify searchability helper

- Extract the duplicated `_document_is_searchable`/`_note_is_searchable`
  helpers into a shared, Playwright-free `tests/integration/_search_helpers.py`
  (`document_is_searchable`), used by both the plotly and sampling tests.
- Resolve the sampling Medium finding: `wait_for_vector_sync` now triggers the
  searchability path on `search_term` alone (matching the plotly variant)
  instead of requiring both `search_term` and `note_id`, removing the silent
  fall-through to the unreliable gauge-delta path.
- Tighten `_get_with_retry`'s `last_exc` annotation to `httpx.TransportError`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Chris Coutinho
2026-06-17 22:49:16 +02:00
co-authored by Claude Opus 4.8
parent 3e8ec2fccd
commit eefa326c09
4 changed files with 56 additions and 60 deletions
@@ -28,6 +28,7 @@ from playwright.async_api import Page
# Import helper functions from existing test
from tests.conftest import create_mcp_client_session
from tests.integration._search_helpers import document_is_searchable
from tests.integration.test_astrolabe_multi_user_background_sync import (
complete_astrolabe_authorization,
login_to_nextcloud,
@@ -38,38 +39,6 @@ logger = logging.getLogger(__name__)
pytestmark = [pytest.mark.integration, pytest.mark.multi_user_basic]
async def _document_is_searchable(
mcp_client, search_term: str, note_id: int | None
) -> bool:
"""Return True once the freshly-created document is retrievable.
Polls ``nc_semantic_search`` (hybrid: an exact unique term reliably matches
on the keyword side) and matches by ``note_id`` when known, otherwise by the
term appearing in a result's title/excerpt.
"""
try:
search = await mcp_client.call_tool(
"nc_semantic_search",
{"query": search_term, "limit": 10, "score_threshold": 0.0},
)
except Exception as e: # transient transport/availability blip — keep polling
logger.debug("Semantic search poll failed: %s", e)
return False
if search.isError:
logger.debug("Semantic search poll error: %s", search)
return False
results = json.loads(search.content[0].text).get("results", [])
needle = search_term.lower()
for r in results:
if note_id is not None:
if r.get("id") == note_id and r.get("doc_type") == "note":
return True
elif needle in f"{r.get('title', '')} {r.get('excerpt', '')}".lower():
return True
return False
async def wait_for_vector_sync(
mcp_client,
initial_indexed_count: int,
@@ -132,7 +101,7 @@ async def wait_for_vector_sync(
)
if search_term is not None:
if await _document_is_searchable(mcp_client, search_term, note_id):
if await document_is_searchable(mcp_client, search_term, note_id):
logger.info(
"✓ Sync complete: document %s retrievable via semantic search",
note_id,