refactor(test): share the Qdrant point-seeding helper

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>
This commit is contained in:
2026-08-22 15:08:25 +03:30
parent 923ac8e5d6
commit 062fdd7ac1
2 changed files with 29 additions and 29 deletions

View File

@@ -18,18 +18,12 @@ from collections.abc import Awaitable, Callable
import pytest import pytest
from qdrant_client import AsyncQdrantClient, models 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.config import QdrantSettings
from src.infrastructure.qdrant.collection import ( from src.infrastructure.qdrant.collection import ensure_chunks_collection
DENSE_NOMIC_DIMENSIONS,
DENSE_OPENAI_DIMENSIONS,
ensure_chunks_collection,
)
from src.infrastructure.qdrant.point_repository import QdrantPointRepository from src.infrastructure.qdrant.point_repository import QdrantPointRepository
from src.infrastructure.qdrant.points import QdrantPointStorage from src.infrastructure.qdrant.points import QdrantPointStorage
from tests.support import point_contract from tests.support import point_contract
from tests.support.point_contract import SeedSpec, build_point, seed_specs from tests.support.point_contract import chunk_point_for, seed_specs
pytestmark = [ pytestmark = [
pytest.mark.integration, pytest.mark.integration,
@@ -54,33 +48,13 @@ IDS = {
} }
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( async def _seeded(
qdrant_client: AsyncQdrantClient, qdrant_settings: QdrantSettings qdrant_client: AsyncQdrantClient, qdrant_settings: QdrantSettings
) -> QdrantPointRepository: ) -> QdrantPointRepository:
await ensure_chunks_collection(qdrant_client, collection=qdrant_settings.collection) await ensure_chunks_collection(qdrant_client, collection=qdrant_settings.collection)
storage = QdrantPointStorage(qdrant_client, collection=qdrant_settings.collection) storage = QdrantPointStorage(qdrant_client, collection=qdrant_settings.collection)
await storage.upsert_points( await storage.upsert_points(
[_chunk_point(spec) for spec in seed_specs(TENANT_A, TENANT_B, FILE_A, FILE_B, FILE_C)] [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 # Payload indexes are built asynchronously; without waiting, a full-text or
# ordered query can run against a half-built index and return short. # ordered query can run against a half-built index and return short.

View File

@@ -21,8 +21,14 @@ from dataclasses import dataclass
from datetime import UTC, datetime from datetime import UTC, datetime
from src.application.ingestion.chunking import chunk_id_for from src.application.ingestion.chunking import chunk_id_for
from src.application.ingestion.models import SparseVector
from src.application.points.models import ChunkPoint
from src.application.points.point import Point from src.application.points.point import Point
from src.application.ports.point_repository import PayloadPatch, PointRepository from src.application.ports.point_repository import PayloadPatch, PointRepository
from src.infrastructure.qdrant.collection import (
DENSE_NOMIC_DIMENSIONS,
DENSE_OPENAI_DIMENSIONS,
)
# Two tenants and two files, so every scenario can assert both that the right # Two tenants and two files, so every scenario can assert both that the right
# rows come back and that the wrong ones do not. # rows come back and that the wrong ones do not.
@@ -76,6 +82,26 @@ def build_point(spec: SeedSpec) -> Point:
) )
def chunk_point_for(spec: SeedSpec) -> ChunkPoint:
"""A `SeedSpec` as something upsertable into real Qdrant.
The payload is taken straight off `build_point`'s read model rather than
hand-written, so the write shape and the read shape cannot drift apart. The
vectors are constant filler: nothing in plan 002's read paths scores by
similarity, so their values are irrelevant and their dimensions are not.
"""
point = build_point(spec)
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=point.model_dump(mode="json", exclude={"point_id", "vectors"}),
)
def seed_specs( def seed_specs(
tenant_a: uuid.UUID, tenant_a: uuid.UUID,
tenant_b: uuid.UUID, tenant_b: uuid.UUID,