"""Meilisearch backend (SPEC §8, M2). Runs against the live Meili in the dev/CI stack; skips if it is unreachable so the rest of the suite still runs locally.""" from __future__ import annotations import asyncio import time import uuid import pytest from app.search.base import SearchDoc from app.search.meili import MeiliSearchBackend from app.search.queryparser import to_search_query def _doc(user_id: uuid.UUID) -> SearchDoc: return SearchDoc( message_id=uuid.uuid4(), thread_id=uuid.uuid4(), user_id=user_id, account_id=uuid.uuid4(), mailbox_special_use="inbox", from_=[{"name": "Bob Vendor", "email": "bob@vendor.com"}], to=[{"name": "Me", "email": "me@x.com"}], cc=[], subject="Your invoice for March", body_text="Please find attached the invoice, amount due soon.", attachment_names=["invoice.pdf"], has_attachments=True, is_unread=True, is_starred=False, date=int(time.time()), size_bytes=2048, ) async def _poll(backend: MeiliSearchBackend, q: str, user_id: uuid.UUID) -> int: """Indexing is async in Meili; retry briefly until the doc is searchable.""" for _ in range(60): hits, _c = await backend.query(to_search_query(q, user_id), limit=10, cursor=None) if hits: return len(hits) await asyncio.sleep(0.05) return 0 async def test_meili_typo_tolerance_and_filters() -> None: backend = MeiliSearchBackend() user_id = uuid.uuid4() try: await backend.index(_doc(user_id)) # Acceptance (§12 M2): a typo still finds the message. hits, _c = await backend.query(to_search_query("invocie", user_id), limit=10, cursor=None) if not hits: # wait for the async index task assert await _poll(backend, "invocie", user_id) == 1 hits, _c = await backend.query( to_search_query("invocie", user_id), limit=10, cursor=None ) assert len(hits) == 1 assert "" in hits[0].highlights.get("subject", "") async def n(query: str) -> int: res, _c = await backend.query(to_search_query(query, user_id), limit=10, cursor=None) return len(res) assert await n("is:unread invoice") == 1 assert await n("is:read invoice") == 0 # the doc is unread assert await n("from:vendor") == 1 # field operator folded into the query assert await n("larger:1k invoice") == 1 assert await n("larger:5k invoice") == 0 assert await n("in:sent invoice") == 0 # doc is in the inbox except (ConnectionError, OSError) as exc: # Meili not running locally pytest.skip(f"Meilisearch unavailable: {exc}") finally: await backend.delete_user(user_id)