Why: - The helper turning a SeedSpec into an upsertable ChunkPoint lived inside one integration test. A second suite now needs to seed real Qdrant the same way, and a copy would let the two drift. Changes: - Move `_chunk_point` into tests/support/point_contract.py as `chunk_point_for`, beside the `build_point` read model it derives its payload from. Impact: - Pure move. No behaviour change; enables the API suite that follows. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
87 lines
3.4 KiB
Python
87 lines
3.4 KiB
Python
"""`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.config import QdrantSettings
|
|
from src.infrastructure.qdrant.collection import 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 chunk_point_for, 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,
|
|
}
|
|
|
|
|
|
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_for(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))
|