Files
Jyotisha/tests/test_consult_card_cache_memory_20261028.py
T
Jesse_ChenandClaude Opus 5.5 18c6e13028
Independent Staging Quality Gate / publish (push) Canceled after 0s
Independent Staging Quality Gate / validate (push) Canceled after 11s
Consult card keeps parsed packets in memory, prewarms each turn, writes 0600 (issue #129, BUG-1476)
- 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>
2026-10-28 11:57:53 +08:00

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"))