"""`QdrantPointRepository` against the shared `PointRepository` contract. The identical scenarios run against the in-memory fake in `tests/unit/application/points/test_point_repository_contract.py`. This file is the half that decides whether the fake is telling the truth: filter construction, `order_by: order_id` scroll, cursor pagination, full-text matching, and the filtered `set_payload` version guard are all Qdrant behaviours a fake can only approximate, and plan 002 Phase 1's exit criterion is that the approximation holds. Seeding differs from the fake's (real points need vectors), so it lives here; everything asserted lives in `tests/support/point_contract.py`. """ import uuid from collections.abc import Awaitable, Callable import pytest from qdrant_client import AsyncQdrantClient, models from src.application.ingestion.models import SparseVector from src.application.points.models import ChunkPoint from src.config import QdrantSettings from src.infrastructure.qdrant.collection import ( DENSE_NOMIC_DIMENSIONS, DENSE_OPENAI_DIMENSIONS, ensure_chunks_collection, ) from src.infrastructure.qdrant.point_repository import QdrantPointRepository from src.infrastructure.qdrant.points import QdrantPointStorage from tests.support import point_contract from tests.support.point_contract import SeedSpec, build_point, seed_specs pytestmark = [ pytest.mark.integration, pytest.mark.qdrant, pytest.mark.asyncio(loop_scope="session"), ] type Scenario = Callable[..., Awaitable[None]] TENANT_A = uuid.UUID("11111111-1111-4111-8111-111111111111") TENANT_B = uuid.UUID("22222222-2222-4222-8222-222222222222") FILE_A = uuid.UUID("33333333-3333-4333-8333-333333333333") FILE_B = uuid.UUID("44444444-4444-4444-8444-444444444444") FILE_C = uuid.UUID("55555555-5555-4555-8555-555555555555") IDS = { "tenant_a": TENANT_A, "tenant_b": TENANT_B, "file_a": FILE_A, "file_b": FILE_B, "file_c": FILE_C, } def _chunk_point(spec: SeedSpec) -> ChunkPoint: """The contract's `Point` re-expressed as something upsertable. `build_point` produces the read model; Qdrant needs vectors and a flat payload dict, so the payload is taken straight off the model to guarantee the two representations cannot drift apart. """ point = build_point(spec) payload = point.model_dump(mode="json", exclude={"point_id", "vectors"}) return ChunkPoint( point_id=point.point_id, dense={ "dense_nomic": [0.1] * DENSE_NOMIC_DIMENSIONS, "dense_openai": [0.2] * DENSE_OPENAI_DIMENSIONS, }, sparse=SparseVector(indices=[1, 2], values=[0.5, 0.25]), payload=payload, ) async def _seeded( qdrant_client: AsyncQdrantClient, qdrant_settings: QdrantSettings ) -> QdrantPointRepository: await ensure_chunks_collection(qdrant_client, collection=qdrant_settings.collection) storage = QdrantPointStorage(qdrant_client, collection=qdrant_settings.collection) await storage.upsert_points( [_chunk_point(spec) for spec in seed_specs(TENANT_A, TENANT_B, FILE_A, FILE_B, FILE_C)] ) # Payload indexes are built asynchronously; without waiting, a full-text or # ordered query can run against a half-built index and return short. await _await_indexing(qdrant_client, qdrant_settings.collection) return QdrantPointRepository(qdrant_client, collection=qdrant_settings.collection) async def _await_indexing(client: AsyncQdrantClient, collection: str) -> None: for _ in range(100): info = await client.get_collection(collection) if info.status == models.CollectionStatus.GREEN and info.indexed_vectors_count is not None: return raise AssertionError(f"collection {collection!r} did not finish indexing") def _arguments(scenario: Scenario) -> dict[str, uuid.UUID]: return {name: value for name, value in IDS.items() if name in scenario.__annotations__} @pytest.mark.parametrize( "scenario", [*point_contract.READ_SCENARIOS, *point_contract.WRITE_SCENARIOS], ids=lambda scenario: scenario.__name__.removeprefix("scenario_"), ) async def test_qdrant_point_repository_satisfies_the_contract( scenario: Scenario, qdrant_client: AsyncQdrantClient, qdrant_settings: QdrantSettings ) -> None: repository = await _seeded(qdrant_client, qdrant_settings) await scenario(repository, **_arguments(scenario))