Files
paperless-ngx/src/documents/tests/search/test_reversed_date_ranges.py
T
stumpylogandClaude Opus 5 2d2dad0e1a test(search): pin currently-unasserted behaviours (Task 10 Add list)
Adds dedicated result-level tests, each indexing real documents and
asserting on matched-ID sets rather than parse shape:

- Deferred `-term` negation (G1): bare `-taxes` requires the term, and
  fielded `-title:alpha` drops the negation entirely, both matching v2.
- Reversed date ranges: absolute bounds swap (whoosh parity); relative
  `now±` bounds day-bump instead, an inconsistency pinned as a
  whoosh-compat follow-up rather than "fixed" locally.
- The six whoosh unit abbreviations (yrs/mos/wks/hrs/mins/secs).
- Unterminated `[` date range brackets 400 at the API level.
- `tag:foo,bar` comma value lists are conjunctive, with a decoy proving
  `correspondent:foo,bar` is not treated as a list.
- Date keyword phrases (`today`) honour the active (non-UTC) timezone,
  not just relative ranges.
- `_DEFAULT_SEARCH_FIELDS` stays a subset of PUBLIC_FIELDS.
- Every declared JSON subpath is actually written at index time.

Also replaces test_schema.py::TestFastFlagAgreement's tantivy-py
__reduce__() pickling probe with one built on the paperless-owned,
tantivy-independent field_descriptors().

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 10:27:05 -07:00

144 lines
5.2 KiB
Python

"""Reversed date ranges: an internal inconsistency between relative and
absolute bounds, pinned exactly as measured rather than "fixed" here.
Measured end to end (FROZEN_NOW = 2026-06-15T12:00:00Z):
added:[now-1h to now+1h] -> 2h window, correct order
added:[now+1h to now-1h] -> ~22h window, DAY-BUMPED, not swapped
added:[2020-01-01 to 2019-01-01] -> 366 days, SWAPPED to the forward order
added:[2019-01-01 to 2020-01-01] -> 366 days (same result either way)
Absolute reversed ranges swap their bounds back into order, matching
whoosh's own behavior. Relative (``now±``) reversed ranges do not swap --
whoosh-compat's date grammar instead adds a day to the upper bound,
producing a much wider window than either the forward or a swapped
reading would give. This is a library-level inconsistency between the two
range kinds, not an application-level rewrite paperless performs (there is
no reversed-range handling in documents/search/_query.py), so it is not
"fixed" here: fixing it belongs in whoosh-compat's date grammar, not in a
pre-parse rewrite on this side (see also the CJK/pre-parse-rewrites
docstrings elsewhere in this suite for the same discipline). Pinned as a
known inconsistency and a library follow-up. When whoosh-compat's date
grammar is fixed to swap consistently, the relative case's test below
inverts -- that inversion is the intended, visible signal.
"""
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
class TestRelativeReversedRangeDayBumpsInsteadOfSwapping:
@pytest.fixture
def docs(self, backend: TantivyBackend) -> dict[str, int]:
with time_machine.travel(FROZEN_NOW, tick=False):
return {
# Inside the correct (forward) 2h window [11:00, 13:00],
# outside the day-bumped window [13:00, next-day 11:00).
"in_forward_window": _index(
backend,
title="Forward window doc",
content="x",
checksum="reversed-relative-forward",
added=FROZEN_NOW,
).pk,
# Outside the correct 2h window, inside the day-bumped
# window the reversed query actually produces.
"in_daybumped_window": _index(
backend,
title="Day-bumped window doc",
content="x",
checksum="reversed-relative-daybumped",
added=datetime(2026, 6, 16, 8, 0, tzinfo=UTC),
).pk,
}
def test_forward_range_matches_only_the_two_hour_window(
self,
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_forward_window"],
}
def test_reversed_range_day_bumps_rather_than_swapping(
self,
backend: TantivyBackend,
docs: dict[str, int],
) -> None:
# If this swapped like the absolute case below, it would match
# "in_forward_window" (the same set as the forward query). It
# instead matches only the day-bumped document -- the documented
# library inconsistency.
with time_machine.travel(FROZEN_NOW, tick=False):
assert _matched_ids(backend, "added:[now+1h to now-1h]") == {
docs["in_daybumped_window"],
}
class TestAbsoluteReversedRangeSwaps:
@pytest.fixture
def docs(self, backend: TantivyBackend) -> dict[str, int]:
return {
"in_range": _index(
backend,
title="In-range doc",
content="x",
checksum="reversed-absolute-in-range",
added=datetime(2019, 6, 1, tzinfo=UTC),
).pk,
"out_of_range": _index(
backend,
title="Out-of-range doc",
content="x",
checksum="reversed-absolute-out-of-range",
added=datetime(2018, 6, 1, tzinfo=UTC),
).pk,
}
def test_forward_range_matches_the_in_range_document(
self,
backend: TantivyBackend,
docs: dict[str, int],
) -> None:
assert _matched_ids(
backend,
"added:[2019-01-01 to 2020-01-01]",
) == {docs["in_range"]}
def test_reversed_range_swaps_to_match_the_same_document(
self,
backend: TantivyBackend,
docs: dict[str, int],
) -> None:
assert _matched_ids(
backend,
"added:[2020-01-01 to 2019-01-01]",
) == {docs["in_range"]}