#!/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("", s) s = AGE_RE.sub('"age_seconds": ', s) return AGE_S_RE.sub("(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())