f4dea53502
CI / compile (pull_request) Successful in 8s
CI / unit (pull_request) Successful in 26s
CI / integration (pull_request) Successful in 26s
build / build (push) Successful in 25s
CI / compile (push) Successful in 9s
CI / unit (push) Successful in 27s
CI / integration (push) Successful in 27s
A repeat of the same query (whitespace/case-normalised, scoped by deep-vs-shallow) returns the stored hits and skips the whole Crossref call and DB scan. Disk-backed (survives restart), TTL'd (CONJURER_LIBRARIAN_CACHE_TTL, default 7d; 0 disables) and size-bounded (CONJURER_LIBRARIAN_CACHE_MAX, default 500). Reuses DiskQueue, so it's a handful of lines. Nothing fancy - exact (normalised) match, not fuzzy. Checked before Crossref only on a fresh search (a resume from checkpoint still continues its scan), and stored after a completed search. Tests: hit/miss, normalisation, deep/shallow separation, expiry, disable, prune. Suite: 58 unit + 59 integration green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
71 lines
2.2 KiB
Python
71 lines
2.2 KiB
Python
"""Integration: the librarian's simple result cache.
|
|
|
|
A repeat of the same query (normalised) returns stored hits and skips the whole
|
|
Crossref + DB scan. Nothing fancy: whitespace/case-insensitive exact match,
|
|
disk-backed, TTL'd, size-bounded, deep/shallow kept separate.
|
|
"""
|
|
import sys
|
|
import types
|
|
|
|
import pytest
|
|
|
|
if "habanero" not in sys.modules:
|
|
_habanero = types.ModuleType("habanero")
|
|
_habanero.Crossref = object
|
|
sys.modules["habanero"] = _habanero
|
|
|
|
import conjurer_librarian as lib # noqa: E402
|
|
from durable_queue import DiskQueue # noqa: E402
|
|
|
|
|
|
@pytest.fixture
|
|
def cache(tmp_path, monkeypatch):
|
|
box = DiskQueue(str(tmp_path / "cache"))
|
|
monkeypatch.setattr(lib, "_cache", box)
|
|
monkeypatch.setattr(lib, "CACHE_TTL_SECONDS", 3600)
|
|
monkeypatch.setattr(lib, "CACHE_MAX_ENTRIES", 500)
|
|
return box
|
|
|
|
|
|
_HITS = {"10.1000/x": {"Title": ["A Paper"], "type": "journal-article"}}
|
|
|
|
|
|
def test_put_then_get_is_a_hit(cache):
|
|
lib._cache_put("kwas foliowy", False, _HITS)
|
|
assert lib._cache_get("kwas foliowy", False) == _HITS
|
|
|
|
|
|
def test_different_query_misses(cache):
|
|
lib._cache_put("kwas foliowy", False, _HITS)
|
|
assert lib._cache_get("witamina c", False) is None
|
|
|
|
|
|
def test_normalised_case_and_whitespace_hit_same_entry(cache):
|
|
lib._cache_put(" Kwas Foliowy ", False, _HITS)
|
|
assert lib._cache_get("kwas foliowy", False) == _HITS
|
|
|
|
|
|
def test_deep_and_shallow_are_cached_separately(cache):
|
|
lib._cache_put("q", False, _HITS)
|
|
assert lib._cache_get("q", True) is None # a deep search is a different key
|
|
assert lib._cache_get("q", False) == _HITS
|
|
|
|
|
|
def test_expired_entry_is_a_miss(cache):
|
|
cache.put(lib._cache_key("q", False), {"query": "q", "final_result": _HITS, "expires": 0})
|
|
assert lib._cache_get("q", False) is None
|
|
|
|
|
|
def test_ttl_zero_disables_the_cache(cache, monkeypatch):
|
|
monkeypatch.setattr(lib, "CACHE_TTL_SECONDS", 0)
|
|
lib._cache_put("q", False, _HITS) # no-op when disabled
|
|
assert len(cache) == 0
|
|
assert lib._cache_get("q", False) is None
|
|
|
|
|
|
def test_prune_bounds_the_cache(cache, monkeypatch):
|
|
monkeypatch.setattr(lib, "CACHE_MAX_ENTRIES", 3)
|
|
for i in range(6):
|
|
lib._cache_put(f"query-{i}", False, _HITS)
|
|
assert len(cache) <= 3
|