test(points): hold the fake and the Qdrant adapter to one shared contract
Why: - Two parallel test files let a fake drift more permissive than the store it stands in for, so unit tests stay green while production diverges. Plan 002 Phase 1's exit criterion is precisely that the two agree. Changes: - One scenario suite in tests/support/point_contract.py, run against FakePointRepository (unit) and QdrantPointRepository (integration). A divergence fails one of the two runs rather than hiding. - The fake models the behaviours services branch on: the implied is_active read filter, value-based cursor pagination, and a stale version guard that matches nothing rather than raising -- the no-op Qdrant's filtered set_payload actually has, and the reason a service must read back to know its write landed. - Patched points are re-validated rather than model_copy'd, so the fake holds a datetime where a read from real Qdrant returns one. - The seeded corpus gives each tenant its own file: point IDs derive from file_id plus chunk_index alone, so two tenants in one file would collide on a single ID and the fixture would assert an impossible state. Impact: - 15 scenarios pass against both implementations. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
"""`FakePointRepository` against the shared `PointRepository` contract.
|
||||
|
||||
The same scenarios run against real Qdrant in
|
||||
`tests/integration/qdrant/test_point_repository.py`. Keeping both runs green is
|
||||
what stops the fake from drifting into something more permissive than the store
|
||||
it stands in for — see `tests/support/point_contract.py`.
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from collections.abc import Awaitable, Callable
|
||||
|
||||
import pytest
|
||||
|
||||
from src.application.ports.point_repository import PointRepository
|
||||
from tests.fakes import FakePointRepository
|
||||
from tests.support import point_contract
|
||||
from tests.support.point_contract import build_point, seed_specs
|
||||
|
||||
pytestmark = [pytest.mark.unit, pytest.mark.asyncio]
|
||||
|
||||
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 _seeded() -> FakePointRepository:
|
||||
repository = FakePointRepository()
|
||||
for spec in seed_specs(TENANT_A, TENANT_B, FILE_A, FILE_B, FILE_C):
|
||||
repository.add(build_point(spec))
|
||||
return repository
|
||||
|
||||
|
||||
def _arguments(scenario: Scenario) -> dict[str, uuid.UUID]:
|
||||
"""Pass only the ids a scenario declares, so each one names its own inputs."""
|
||||
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_fake_point_repository_satisfies_the_contract(scenario: Scenario) -> None:
|
||||
repository: PointRepository = _seeded()
|
||||
await scenario(repository, **_arguments(scenario))
|
||||
Reference in New Issue
Block a user