test(search): harden coverage for aliases, fast flags, and date edges

Four targeted additions, no production code:

The type-alias test asserted only that a query object was built, and a
naive result-level replacement turned out equally vacuous for a subtle
reason: document_type is itself a default search field, so a broken
alias resolution demoting "type:invoice" to unfielded text STILL
matches the typed document through the field value under test. Both
alias tests (type/document_type, path/storage_path) now use
discriminating decoys carrying the query word in content, so demotion
matches the decoy and fails the exact-set assertion; the old
parse-shape test is deleted.

A new schema test pins that every PublicField.fast flag equals the
built tantivy schema's per-field fast option, in both drift directions:
whoosh-compat trusts the declared flag when resolving field:* existence
checks, and build_schema() only honors it for U64 and DATE kinds, so a
future fast=True TEXT/KEYWORD/JSON entry would otherwise make those
searches silently match nothing at query time.

Two result-level date pins restore behaviors whose assertions were lost
in the test migration: a created date matches regardless of the active
timezone (the America/New_York leg is the discriminating one: a
tz-applying implementation shifts the window past the naive-midnight
indexed value), and a reversed created:[2025 TO 2020] range still
matches its span through the joint-disambiguation swap.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WMsn6DgzbvSqh1pwy66VVF
This commit is contained in:
Trenton Holmes
2026-08-18 11:05:04 -07:00
co-authored by Claude Fable 5
parent 1cb07030b0
commit 2aee7f9c74
3 changed files with 134 additions and 9 deletions
@@ -583,3 +583,115 @@ class TestBareJsonFieldPrefixes:
backend.add_or_update(doc)
assert _matched_ids(backend, "notes.user:bob") == {doc.pk}
assert _matched_ids(backend, "notes.note:remark") == {doc.pk}
class TestFieldAliases:
"""type:/path: are registry aliases for document_type:/storage_path:.
The only other alias coverage is parse-shape; these prove resolution
end-to-end against a real index."""
def test_type_alias_and_canonical_name_match_the_same_document(
self,
backend: TantivyBackend,
) -> None:
from documents.models import DocumentType
invoice_type = DocumentType.objects.create(name="invoice")
# Discriminating shape: document_type is itself a default search
# field, so if alias resolution ever broke and "type:invoice"
# demoted to unfielded text, the token would STILL match the typed
# document through the field value. The decoy carries the query
# word in content, so a demoted search matches BOTH documents and
# the exact-set assertions fail. (The title avoids stemming to
# "type": english stems Typed -> type.)
typed = Document.objects.create(
title="First",
content="quarterly statement",
checksum="alias-type-1",
document_type=invoice_type,
)
decoy = Document.objects.create(
title="Second",
content="invoice mentioned in body",
checksum="alias-type-2",
)
backend.add_or_update(typed)
backend.add_or_update(decoy)
assert _matched_ids(backend, "type:invoice") == {typed.pk}
assert _matched_ids(backend, "document_type:invoice") == {typed.pk}
def test_path_alias_and_canonical_name_match_the_same_document(
self,
backend: TantivyBackend,
) -> None:
from documents.models import StoragePath
archive = StoragePath.objects.create(name="archive", path="archive/{title}")
stored = Document.objects.create(
title="Stored",
content="quarterly statement",
checksum="alias-path-1",
storage_path=archive,
)
# storage_path is NOT a default search field today, so a demoted
# "path:archive" already matches nothing; the content decoy keeps
# this test discriminating even if it ever joins the defaults.
loose = Document.objects.create(
title="Loose",
content="archive mentioned in body",
checksum="alias-path-2",
)
backend.add_or_update(stored)
backend.add_or_update(loose)
assert _matched_ids(backend, "path:archive") == {stored.pk}
assert _matched_ids(backend, "storage_path:archive") == {stored.pk}
class TestCreatedTimezoneInvariance:
def test_created_date_matches_regardless_of_active_timezone(
self,
backend: TantivyBackend,
) -> None:
# "created" is a date-only field indexed at naive midnight: a
# document created 2020-06-10 must match created:20200610 whether
# the active timezone is far ahead of or behind UTC. (The
# timezone-SENSITIVE datetime fields have their own boundary
# coverage in test_api_search.py's tz-ahead/tz-behind tests.)
from django.utils import timezone as django_tz
doc = Document.objects.create(
title="Dated",
content="x",
checksum="tz-inv-1",
created=date(2020, 6, 10),
)
backend.add_or_update(doc)
for tzname in ("Pacific/Auckland", "America/New_York"):
with django_tz.override(tzname):
assert _matched_ids(backend, "created:20200610") == {doc.pk}, tzname
class TestReversedDateRange:
def test_reversed_bounds_still_match_the_span(
self,
backend: TantivyBackend,
) -> None:
# whoosh's joint disambiguation swaps backwards bounds (both years
# explicit -> plain swap), and whoosh-compat reproduces it; a saved
# view with created:[2025 TO 2020] must keep matching the span
# instead of becoming an empty lo>hi range.
inside = Document.objects.create(
title="Inside",
content="x",
checksum="rev-range-1",
created=date(2022, 5, 1),
)
outside = Document.objects.create(
title="Outside",
content="x",
checksum="rev-range-2",
created=date(2019, 5, 1),
)
backend.add_or_update(inside)
backend.add_or_update(outside)
assert _matched_ids(backend, "created:[2025 TO 2020]") == {inside.pk}
-9
View File
@@ -126,15 +126,6 @@ class TestParseUserQuery:
kinds = {type(e) for e in exc_info.value.errors}
assert kinds == {InvalidDateQuery, InvalidNumberQuery}
def test_document_type_query_via_type_alias_matches(
self,
query_index: tantivy.Index,
) -> None:
# Field alias handling now goes through the FieldRegistry, not
# FIELD_ALIASES string substitution — prove it still resolves.
q = parse_user_query(query_index, "type:invoice", UTC)
assert isinstance(q, tantivy.Query)
def test_asn_field_is_query_addressable(
self,
query_index: tantivy.Index,
+22
View File
@@ -126,3 +126,25 @@ class TestSchemaMatchesPublicFields:
searcher = index.searcher()
results = searcher.search(tantivy.Query.term_query(schema, "asn", 42), limit=1)
assert len(results.hits) == 1
class TestFastFlagAgreement:
def test_every_public_field_fast_flag_matches_the_built_schema(self) -> None:
# whoosh-compat's registry trusts PublicField.fast when resolving
# field:* existence checks (its FAST_FIELD strategy); a fast=True
# entry whose actual tantivy column is not fast would make those
# searches silently match nothing at search time. build_schema()
# only honors the flag in its U64 and DATE branches today, so this
# pins the agreement for EVERY kind: a future fast=True
# TEXT/KEYWORD/JSON entry the builder silently ignores fails here
# instead of at a user's query.
state = build_schema().__reduce__()[1][0]
schema_fast = {
field["name"]: bool(field["options"].get("fast", False))
for field in state["inner"]
}
for public_field in PUBLIC_FIELDS:
assert schema_fast[public_field.name] == public_field.fast, (
f"{public_field.name}: PublicField.fast={public_field.fast} but the"
f" built schema says fast={schema_fast[public_field.name]}"
)