Files
chatbot_v3/tests/unit/application/points/test_queries.py
Ali Zarinkolah ba7921dd4e test(points): cover the read paths and the query service
Why:
- The isolation and pagination guarantees are the ones that fail silently, so
  they need tests that would actually notice.

Changes:
- API tests over real Postgres and Qdrant together, because the invariant worth
  testing spans both: the tenant Postgres derived is the only one Qdrant is ever
  queried with.
- Pagination holds when a point is inserted behind the cursor mid-listing --
  the defect an offset cursor would have.
- A route-order guard, since /{point_id} declared first turns count into a 422
  and nothing else in the suite would catch it.
- Unit tests for the query service against the fake, including the Arabic to
  Persian letterform fold and raising rather than returning None.

Impact:
- Suite goes to 325 passed, 3 skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 15:09:31 +03:30

104 lines
3.7 KiB
Python

"""`application/points/queries.py` against the fake repository (ADR-0016).
The HTTP-level behaviour is covered in `tests/integration/qdrant/`. What is
worth isolating here is the small amount of logic the service actually owns —
raising instead of returning `None`, and folding a query's Arabic letterforms —
because both are cheap to test directly and expensive to notice when broken.
"""
import uuid
import pytest
from src.application.points.point import PointNotFoundError
from src.application.points.queries import (
count_points,
get_point,
list_file_points,
search_points,
)
from tests.fakes import FakePointRepository
from tests.support.point_contract import SeedSpec, build_point, point_id_for
pytestmark = [pytest.mark.unit, pytest.mark.asyncio]
TENANT = uuid.UUID("11111111-1111-4111-8111-111111111111")
OTHER_TENANT = uuid.UUID("22222222-2222-4222-8222-222222222222")
FILE = uuid.UUID("33333333-3333-4333-8333-333333333333")
def _repository(*specs: SeedSpec) -> FakePointRepository:
repository = FakePointRepository()
for spec in specs:
repository.add(build_point(spec))
return repository
async def test_get_point_returns_the_point_for_its_owner() -> None:
repository = _repository(SeedSpec(TENANT, FILE, 0, "hello", 1.0))
point = await get_point(repository, tenant_id=TENANT, point_id=point_id_for(FILE, 0))
assert point.content == "hello"
async def test_get_point_raises_for_another_tenants_point() -> None:
"""Raising, not returning `None`, so a route cannot serve `200 null`."""
repository = _repository(SeedSpec(TENANT, FILE, 0, "hello", 1.0))
with pytest.raises(PointNotFoundError):
await get_point(repository, tenant_id=OTHER_TENANT, point_id=point_id_for(FILE, 0))
async def test_get_point_raises_for_an_unknown_point() -> None:
repository = _repository()
with pytest.raises(PointNotFoundError):
await get_point(repository, tenant_id=TENANT, point_id=uuid.uuid4())
async def test_search_points_folds_arabic_letterforms_before_matching() -> None:
"""The query is normalized the same way ingestion normalized the content.
Stored content carries Persian yeh (U+06CC) because `normalize_persian_text`
folded it at ingest. A query typed with Arabic yeh (U+064A) is a different
codepoint and would match nothing — silently, with no error to notice.
"""
persian = "مدیریت"
arabic = "مديريت"
assert persian != arabic
repository = _repository(SeedSpec(TENANT, FILE, 0, persian, 1.0))
found = await search_points(repository, tenant_id=TENANT, query=arabic, limit=10)
assert [point.content for point in found.points] == [persian]
async def test_search_points_does_not_reach_another_tenant() -> None:
repository = _repository(SeedSpec(OTHER_TENANT, FILE, 0, "secret", 1.0))
found = await search_points(repository, tenant_id=TENANT, query="secret", limit=10)
assert found.points == ()
async def test_list_file_points_returns_an_empty_page_for_a_foreign_file() -> None:
"""Indistinguishable from an unknown file — the same non-disclosure rule."""
repository = _repository(SeedSpec(OTHER_TENANT, FILE, 0, "secret", 1.0))
page = await list_file_points(repository, tenant_id=TENANT, file_id=FILE, limit=10)
assert page.points == ()
assert page.next_cursor is None
async def test_count_points_excludes_inactive_unless_asked() -> None:
repository = _repository(
SeedSpec(TENANT, FILE, 0, "live", 1.0),
SeedSpec(TENANT, FILE, 1, "gone", 2.0, is_active=False),
)
assert await count_points(repository, tenant_id=TENANT) == 1
assert await count_points(repository, tenant_id=TENANT, include_inactive=True) == 2