Files
rune/scripts/check_cache_parity.py
Nick f944d7df38 feat(cache): close PARTIAL gaps — dichotomy wave
Port the eleven audit-named missing fns name-preserving 1:1 from
src/rune/cache.py (recordHit/_record_hit, recordMiss, flushStats,
getCache, putCache, invalidateScope, invalidateAll, nowTs, parseTs,
cacheStats, handleHook) with the in-memory hit/miss accumulation
(threshold 50 / interval 30s), INVALIDATION_MAP, full schema (both
indexes) and an oracle-byte-parity CLI (usage gates, --json indent=2,
rune-aware :20 padding and [:200] truncation, compact hook line,
PLUGIN manifest). Clock/monotonic/DB behind Env seams.

Frozen truth: 78 cases via the shared freeze_truth.py harness
(envelope, pinned clock, sandboxed RUNE_MEMORY_DIR), byte-identical
regen + --verify green. Go battery replays verbatim + flush-branch
seam pins + 16-case CLI live parity (100%). Python oracle battery 12
passed BOTH pins. Audit: cache 6/17 -> 17/17 FULL (GO).
2026-10-14 02:59:43 +01:00

93 lines
3.2 KiB
Python

#!/usr/bin/env python3
"""check_cache_parity.py — live CLI parity: the Python oracle
(python -m rune.cache) vs the Go port (built binary) over the same argv
matrix, each in its own throwaway memory dir. Byte-for-byte stdout with
ISO timestamps and age values normalized (both engines render real
clocks). Covers every ported fn through the CLI surface: put_cache,
get_cache, invalidate, invalidate_scope, invalidate_all, cache_stats,
handle_hook, main (usage gates, --json/plain rendering, ensure_ascii).
"""
import json
import os
import re
import subprocess
import sys
import tempfile
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
GO_BIN = Path(tempfile.mkdtemp(prefix="cache-parity-")) / "cache"
TS_RE = re.compile(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?\+00:00")
AGE_RE = re.compile(r'"age_seconds": [0-9.e+-]+')
AGE_S_RE = re.compile(r"\(age: \d+s\)")
def normalize(s: str) -> str:
s = TS_RE.sub("<TS>", s)
s = AGE_RE.sub('"age_seconds": <AGE>', s)
return AGE_S_RE.sub("(age: <AGE>s", s)
MATRIX = [
["put", "alpha", "Alpha value line one\nAlpha line two"],
["put", "beta", "Beta retrieval value", "--scope", "retrieval", "--source-hash", "src-1", "--json"],
["put", "uni-ü", "Wörter with ümlaut", "--json"],
["get", "alpha", "--json"],
["get", "alpha"],
["get", "uni-ü", "--json"],
["get", "missing-key"],
["get"],
["put", "only-key"],
["invalidate", "alpha", "--reason", "stale", "--json"],
["invalidate-scope", "retrieval", "--json"],
["invalidate-all", "--json"],
["stats"],
["stats", "--json"],
["hook", "--event", "memory.created", "--data", '{"x": 1}'],
["hook"],
]
def py_run(argv: list[str], mem: str) -> str:
env = dict(os.environ, RUNE_MEMORY_DIR=mem, PYTHONPATH=str(REPO / "src"))
r = subprocess.run([sys.executable, "-m", "rune.cache", *argv],
capture_output=True, text=True, cwd=REPO, env=env)
if r.returncode != 0:
sys.exit(f"oracle CLI {argv} failed: {r.stderr}")
return r.stdout
def go_run(argv: list[str], mem: str) -> str:
env = dict(os.environ, RUNE_MEMORY_DIR=mem)
r = subprocess.run([str(GO_BIN), *argv], capture_output=True, text=True, cwd=REPO, env=env)
if r.returncode != 0:
sys.exit(f"go CLI {argv} failed: {r.stderr}")
return r.stdout
def main() -> int:
build = subprocess.run(["go", "build", "-o", str(GO_BIN), "."],
cwd=REPO / "src/go/cache", capture_output=True, text=True)
if build.returncode != 0:
sys.exit(f"go build failed: {build.stderr}")
py_mem = tempfile.mkdtemp(prefix="cache-parity-py-")
go_mem = tempfile.mkdtemp(prefix="cache-parity-go-")
passed = 0
for argv in MATRIX:
py_out = normalize(py_run(argv, py_mem))
go_out = normalize(go_run(argv, go_mem))
if py_out != go_out:
print(f"FAIL {' '.join(argv)}\n py: {py_out!r}\n go: {go_out!r}")
return 1
passed += 1
print(f"PASS {' '.join(json.dumps(a, ensure_ascii=False) for a in argv)}")
print(f"LIVE PARITY 100% ({passed}/{len(MATRIX)} cases)")
return 0
if __name__ == "__main__":
sys.exit(main())