From caf87136f6f8aa25234f0c2173bd1233147086e7 Mon Sep 17 00:00:00 2001 From: stumpylog <797416+stumpylog@users.noreply.github.com> Date: Thu, 20 Aug 2026 15:49:31 -0700 Subject: [PATCH] test(search): collapse the date survivors onto one representative Follow-up to 98bd31f7d, applying the stricter reading of "one representative case per integration seam". The trimmed unit-abbreviation and reversed-range survivors reached the same seams as test_compact_date_forms.py by different spellings: no mutation kills either without also killing the compact case, and the compact case additionally fails when queries are resolved in a fixed non-local timezone, which neither of the other two catches because now-5h and now+/-1h are timezone-invariant. Verified before deleting: the compact survivor fails under all three mutations (fixed non-local timezone, `added` registered as TEXT instead of DATETIME, `added` indexed at day precision). The spellings themselves are asserted by the library, per unit in tests/test_relative_date_unit_abbreviations.py and for both range kinds in tests/test_parser_dates.py. The reversed-range file's other purpose, to invert as the signal that the library's bound swap landed, is spent: it inverted. Also give TestTagCommaList a docstring saying what it is. It is a docs-contract regression test for a published spelling, not proof of paperless's field configuration: removing comma_values from the tag FieldSpec leaves it passing, because the analyzer splits the literal value into the same tokens. The registry fact is owned by test_registry.py. Co-Authored-By: Claude Opus 5 --- .../tests/search/test_documented_syntax.py | 14 ++++ .../tests/search/test_reversed_date_ranges.py | 81 ------------------- .../search/test_whoosh_unit_abbreviations.py | 76 ----------------- 3 files changed, 14 insertions(+), 157 deletions(-) delete mode 100644 src/documents/tests/search/test_reversed_date_ranges.py delete mode 100644 src/documents/tests/search/test_whoosh_unit_abbreviations.py diff --git a/src/documents/tests/search/test_documented_syntax.py b/src/documents/tests/search/test_documented_syntax.py index 0fea3f0c5..24dd25f45 100644 --- a/src/documents/tests/search/test_documented_syntax.py +++ b/src/documents/tests/search/test_documented_syntax.py @@ -113,6 +113,20 @@ class TestPhraseSearch: class TestTagCommaList: + """``tag:bills,unpaid`` is published syntax (docs/usage.md), so this checks + that the documented spelling still returns what the docs promise: only the + document carrying every listed tag. + + It is deliberately not proof of paperless's field configuration, and must + not be read as such. Removing ``comma_values`` from the ``tag`` FieldSpec + leaves this test passing, because paperless's analyzer splits the literal + value "bills,unpaid" into the same two tokens the value-list reading + produces, so the two readings select the same documents. The registry fact + -- that ``tag`` opts in and no other field does -- is observable only at + the registry, and is owned by test_registry.py's + ``test_tag_is_comma_values``/``test_correspondent_is_not_comma_values``. + """ + def test_comma_list_requires_every_listed_tag( self, backend: TantivyBackend, diff --git a/src/documents/tests/search/test_reversed_date_ranges.py b/src/documents/tests/search/test_reversed_date_ranges.py deleted file mode 100644 index 4f252050b..000000000 --- a/src/documents/tests/search/test_reversed_date_ranges.py +++ /dev/null @@ -1,81 +0,0 @@ -"""A reversed relative range, resolved end to end against the index. - -whoosh-compat owns the bound swap itself and asserts it directly, for both -the absolute and the relative spelling -(``test_reversed_relative_range_swaps_like_the_absolute_case`` in -``tests/test_parser_dates.py``, DIVERGENCES.md entry 53). Paperless does not -rewrite reversed ranges anywhere; ``documents/search/_query.py`` passes the -query through untouched. - -What is kept here is the relative spelling only, because it is the one whose -bounds depend on paperless's own plumbing: ``now±1h`` is resolved against the -timezone paperless hands the parser, so the emitted window is paperless's -result rather than the library's alone. The corpus separates a document -inside the two-hour window from one that is outside it but inside the wider -window a non-swapping reading produces, so a bound that resolves in the wrong -timezone, or a swap that does not happen, matches the wrong set. -""" - -from __future__ import annotations - -from datetime import UTC -from datetime import datetime -from typing import TYPE_CHECKING - -import pytest -import time_machine - -from documents.models import Document - -if TYPE_CHECKING: - from documents.search._backend import TantivyBackend - -pytestmark = [pytest.mark.search, pytest.mark.django_db] - -FROZEN_NOW = datetime(2026, 6, 15, 12, 0, tzinfo=UTC) - - -def _matched_ids(backend: TantivyBackend, query: str) -> set[int]: - return set(backend.search_ids(query, user=None)) - - -def _index(backend: TantivyBackend, **kwargs: object) -> Document: - doc = Document.objects.create(**kwargs) - backend.add_or_update(doc) - return doc - - -@pytest.fixture -def docs(backend: TantivyBackend) -> dict[str, int]: - with time_machine.travel(FROZEN_NOW, tick=False): - return { - # Inside the 2h window [11:00, 13:00] that both the forward and - # the reversed spelling resolve to. - "in_window": _index( - backend, - title="Forward window doc", - content="x", - checksum="reversed-relative-forward", - added=FROZEN_NOW, - ).pk, - # Outside that window, but inside the wider window a reversed - # range that day-bumped its upper bound instead of swapping - # would reach. - "outside_the_window": _index( - backend, - title="Later same-week doc", - content="x", - checksum="reversed-relative-outside", - added=datetime(2026, 6, 16, 8, 0, tzinfo=UTC), - ).pk, - } - - -def test_reversed_relative_range_matches_the_two_hour_window( - backend: TantivyBackend, - docs: dict[str, int], -) -> None: - with time_machine.travel(FROZEN_NOW, tick=False): - assert _matched_ids(backend, "added:[now+1h to now-1h]") == { - docs["in_window"], - } diff --git a/src/documents/tests/search/test_whoosh_unit_abbreviations.py b/src/documents/tests/search/test_whoosh_unit_abbreviations.py deleted file mode 100644 index 59c472314..000000000 --- a/src/documents/tests/search/test_whoosh_unit_abbreviations.py +++ /dev/null @@ -1,76 +0,0 @@ -"""One relative unit abbreviation, resolved end to end against the index. - -whoosh-compat owns which unit words exist and what offset each resolves to, -and asserts all six (yrs/mos/wks/hrs/mins/secs, quoted and bare) directly in -``tests/test_relative_date_unit_abbreviations.py``. Repeating that matrix here -would only re-prove the library's grammar; "hrs" is kept as a single -representative so that the paperless-side path the library cannot see -- the -``added`` DATETIME fast field, the timezone the query is resolved in and the -range the backend emits -- is still exercised by an abbreviation-spelled -offset rather than only by absolute dates. - -The offset is sub-day on purpose: a whole-day or whole-year offset survives a -wrong timezone, five hours does not. The decoy document sits at a different -offset from the same instant, so an offset that resolves to the wrong width -lands on nothing or on the decoy instead of passing by accident. -""" - -from __future__ import annotations - -from datetime import UTC -from datetime import datetime -from typing import TYPE_CHECKING - -import pytest -import time_machine - -from documents.models import Document - -if TYPE_CHECKING: - from documents.search._backend import TantivyBackend - -pytestmark = [pytest.mark.search, pytest.mark.django_db] - -FROZEN_NOW = datetime(2026, 6, 15, 12, 0, tzinfo=UTC) - - -def _matched_ids(backend: TantivyBackend, query: str) -> set[int]: - return set(backend.search_ids(query, user=None)) - - -def _index(backend: TantivyBackend, **kwargs: object) -> Document: - doc = Document.objects.create(**kwargs) - backend.add_or_update(doc) - return doc - - -@pytest.fixture -def unit_documents(backend: TantivyBackend) -> dict[str, int]: - """One document five hours before the frozen instant, one ten minutes - before it, both indexed together so a mis-resolved offset lands on the - wrong document or on none rather than passing coincidentally.""" - with time_machine.travel(FROZEN_NOW, tick=False): - return { - "hrs": _index( - backend, - title="Hours doc", - content="x", - checksum="unit-abbrev-hrs", - added=datetime(2026, 6, 15, 7, 0, tzinfo=UTC), - ).pk, - "mins": _index( - backend, - title="Minutes doc", - content="x", - checksum="unit-abbrev-mins", - added=datetime(2026, 6, 15, 11, 50, tzinfo=UTC), - ).pk, - } - - -def test_hours_abbreviation_matches_only_its_own_instant( - backend: TantivyBackend, - unit_documents: dict[str, int], -) -> None: - with time_machine.travel(FROZEN_NOW, tick=False): - assert _matched_ids(backend, 'added:"-5hrs"') == {unit_documents["hrs"]}