- consult_packet_view keeps the last 3 parsed packets with their shared domain base (~30 MB each). A hit reads no file and copies nothing: attach for one domain 0.106 s -> 0.002 s. Memory holds the file's JSON round trip, i.e. exactly what a hit returned before; consult_card_facts sha256 equal old/new. - Cache files are created 0600, same bytes as before. - The consult route fires a warm for the turn's chart before reserving credit, so the day's first question does not build the packet on the answer path. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
120 lines
4.8 KiB
Python
120 lines
4.8 KiB
Python
"""Issue #129: the consult card keeps parsed packets in memory, files are 0600."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import copy
|
|
import hashlib
|
|
import json
|
|
import stat
|
|
import uuid
|
|
from pathlib import Path
|
|
|
|
from consult_card_domain_facts import build_consult_card_facts
|
|
import consult_full_data_cache as cache
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
DENSITY = ROOT / "frontend" / "tests" / "fixtures" / "report-density-fictional-engine.json"
|
|
DOMAINS = ["career", "wealth", "marriage", "health"]
|
|
|
|
|
|
def _identity(minute: int = 1) -> dict:
|
|
return {
|
|
"schema": cache.CACHE_SCHEMA,
|
|
"year": 1901, "month": 1, "day": 1, "hour": 0, "minute": minute, "second": 0,
|
|
"lat": 0, "lon": 0, "tz": 0, "ayanamsa": "raman", "node_mode": "mean",
|
|
"engine_version": f"test-{uuid.uuid4().hex}",
|
|
"reference_date": "2026-10-28",
|
|
}
|
|
|
|
|
|
def _builder(_identity: dict) -> dict:
|
|
return json.loads(DENSITY.read_text(encoding="utf-8"))
|
|
|
|
|
|
def _fingerprint(value) -> str:
|
|
return hashlib.sha256(json.dumps(value, ensure_ascii=False, sort_keys=True, default=str).encode()).hexdigest()
|
|
|
|
|
|
def _facts_the_old_way(identity: dict) -> str:
|
|
# Before #129: every turn parsed the file and built facts without a shared base.
|
|
packet = json.loads(cache._cache_path(identity).read_text(encoding="utf-8"))
|
|
return json.dumps(build_consult_card_facts(packet, DOMAINS, minute_confirmed=True), ensure_ascii=False)
|
|
|
|
|
|
def test_memory_hits_give_byte_identical_facts_and_skip_the_file():
|
|
cache.clear_memory_cache()
|
|
identity = _identity()
|
|
first = cache.consult_packet_view(identity, _builder)
|
|
assert first["hit"] is False and first["memory"] is False
|
|
expected = _facts_the_old_way(identity)
|
|
from_build = build_consult_card_facts(first["packet"], DOMAINS, minute_confirmed=True, base=first["base"])
|
|
assert json.dumps(from_build, ensure_ascii=False) == expected
|
|
|
|
# The file is no longer read on a memory hit.
|
|
path = cache._cache_path(identity)
|
|
saved = path.read_bytes()
|
|
path.unlink()
|
|
try:
|
|
second = cache.consult_packet_view(identity, _builder)
|
|
third = cache.consult_packet_view(identity, _builder)
|
|
finally:
|
|
path.write_bytes(saved)
|
|
assert second["hit"] is True and second["memory"] is True
|
|
assert second["packet"] is third["packet"] and second["base"] is third["base"]
|
|
for view in (second, third):
|
|
facts = build_consult_card_facts(view["packet"], DOMAINS, minute_confirmed=True, base=view["base"])
|
|
assert json.dumps(facts, ensure_ascii=False) == expected
|
|
|
|
|
|
def test_reading_facts_never_changes_the_shared_packet_or_base():
|
|
cache.clear_memory_cache()
|
|
identity = _identity()
|
|
cache.consult_packet_view(identity, _builder)
|
|
view = cache.consult_packet_view(identity, _builder)
|
|
before = (_fingerprint(view["packet"]), _fingerprint(view["base"]))
|
|
for _ in range(3):
|
|
facts = build_consult_card_facts(view["packet"], DOMAINS, minute_confirmed=False, base=view["base"])
|
|
# A caller that edits its facts must not reach the shared objects.
|
|
for row in facts["domains"]:
|
|
row.clear()
|
|
assert (_fingerprint(view["packet"]), _fingerprint(view["base"])) == before
|
|
|
|
|
|
def test_memory_keeps_the_newest_charts_only():
|
|
cache.clear_memory_cache()
|
|
identities = [_identity(minute) for minute in range(1, cache.MEMORY_SLOTS + 2)]
|
|
for identity in identities:
|
|
cache.consult_packet_view(identity, _builder)
|
|
assert len(cache._memory) == cache.MEMORY_SLOTS
|
|
assert cache.cache_key(identities[0]) not in cache._memory
|
|
assert cache.cache_key(identities[-1]) in cache._memory
|
|
|
|
|
|
def test_new_cache_files_are_private():
|
|
identity = _identity()
|
|
cache.get_or_build(identity, _builder)
|
|
mode = stat.S_IMODE(cache._cache_path(identity).stat().st_mode)
|
|
assert mode == 0o600, oct(mode)
|
|
# Same bytes as before: compact, key order as built (not sorted).
|
|
expected = json.dumps(_builder(identity), ensure_ascii=False, separators=(",", ":"))
|
|
assert cache._cache_path(identity).read_text(encoding="utf-8") == expected
|
|
|
|
|
|
def test_a_miss_answers_from_the_build_and_remembers_the_file_copy():
|
|
cache.clear_memory_cache()
|
|
built = {"value": None}
|
|
|
|
def builder(identity: dict) -> dict:
|
|
packet = _builder(identity)
|
|
packet["worksheets"]["tuple_probe"] = ("a", "b")
|
|
built["value"] = packet
|
|
return packet
|
|
|
|
identity = _identity()
|
|
first = cache.consult_packet_view(identity, builder)
|
|
assert first["packet"] is built["value"]
|
|
remembered = cache.consult_packet_view(identity, builder)["packet"]
|
|
assert remembered is not built["value"]
|
|
assert remembered["worksheets"]["tuple_probe"] == ["a", "b"], "what a file hit always returned"
|
|
assert copy.deepcopy(remembered) == json.loads(cache._cache_path(identity).read_text(encoding="utf-8"))
|