Round-10 review nit: match the defensive `str(id) == str(note_id)` pattern used by _poll_astrolabe_search_for_note. nc_semantic_search returns int ids today (behaviour-neutral now), but the coercion guards against a future schema change serialising ids as strings, which would otherwise silently break the match and time out with a generic message. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
69 lines
2.8 KiB
Python
69 lines
2.8 KiB
Python
"""Shared helpers for asserting vector-sync visibility in integration tests.
|
|
|
|
Kept dependency-light (no Playwright) so both the multi-user-basic UI tests and
|
|
the single-user sampling tests can import it.
|
|
"""
|
|
|
|
import json
|
|
import logging
|
|
from typing import Any
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
async def document_is_searchable(
|
|
mcp_client: Any, search_term: str, note_id: int | None = None
|
|
) -> bool:
|
|
"""Return True once a 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 provided, otherwise by
|
|
the term appearing in a result's title/excerpt. Transient errors return
|
|
False so callers can keep polling.
|
|
"""
|
|
try:
|
|
search = await mcp_client.call_tool(
|
|
"nc_semantic_search",
|
|
# limit is generous: a fresh note can sit below seed data (e.g. deck
|
|
# cards) in a crowded corpus, and the query is cheap.
|
|
arguments={"query": search_term, "limit": 50, "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
|
|
|
|
try:
|
|
results = json.loads(search.content[0].text).get("results", [])
|
|
except (IndexError, ValueError) as e: # empty content / malformed JSON
|
|
logger.debug("Semantic search parse failed: %s", e)
|
|
return False
|
|
|
|
# Token match (not contiguous substring) so multi-word terms work in the
|
|
# note_id-less fallback path.
|
|
tokens = search_term.lower().split()
|
|
for r in results:
|
|
if note_id is not None:
|
|
# str-coerce both sides: nc_semantic_search returns int ids today,
|
|
# but the Astrolabe API serialises some ids as strings — match the
|
|
# defensive comparison in _poll_astrolabe_search_for_note so a future
|
|
# schema change can't silently break the match.
|
|
if str(r.get("id")) == str(note_id):
|
|
if r.get("doc_type") == "note":
|
|
return True
|
|
# id matched but not a note — surface possible schema drift at
|
|
# WARNING (CI runs --log-cli-level=WARN) instead of letting the
|
|
# caller time out with a generic message.
|
|
logger.warning(
|
|
"search hit id=%s has doc_type=%s (expected note)",
|
|
note_id,
|
|
r.get("doc_type"),
|
|
)
|
|
else:
|
|
haystack = f"{r.get('title', '')} {r.get('excerpt', '')}".lower()
|
|
if tokens and all(t in haystack for t in tokens):
|
|
return True
|
|
return False
|