#!/usr/bin/env python3 """Structural + value parity check for the tracker-go read API vs the Python service. Run on the server (loopback access to both, plus `docker exec dereth-db` for the offline-character exact-match check): python3 compare_endpoints.py Most live endpoints can't be value-equal byte-for-byte (the firehose updates between fetches), so we assert: * status code + top-level key-set parity for every read endpoint, and * EXACT equality of /character-stats and /combat-stats for *offline* characters (where Python also falls back to the DB, like Go). For online characters Python serves a richer live in-memory overlay that Phase-1 Go intentionally lacks (no ingest yet) — that difference is expected. """ import json import subprocess import sys import urllib.error import urllib.parse import urllib.request PY = "http://127.0.0.1:8765" GO = "http://127.0.0.1:8770" def get(base, path): try: with urllib.request.urlopen(base + path, timeout=12) as r: return r.status, json.load(r) except urllib.error.HTTPError as e: return e.code, None except Exception as e: # noqa: BLE001 return "ERR:" + str(e)[:40], None def topkeys(d): if isinstance(d, dict): return sorted(d.keys()) if isinstance(d, list): return ["[list]"] return [type(d).__name__] def main(): failures = 0 _, live = get(PY, "/live") ch = live["players"][0]["character_name"] if live and live.get("players") else "Nobody" chq = urllib.parse.quote(ch) print(f"sample online character: {ch}\n") endpoints = [ "/total-rares", "/total-kills", "/server-health", "/portals", "/spawns/heatmap?hours=2", "/combat-stats", "/inventories", "/quest-status", "/vital-sharing/peers", f"/stats/{chq}", f"/combat-stats/{chq}", f"/inventory/{chq}/search", "/sets/list", "/inventory-characters", ] print(f"{'endpoint':<36} {'py':>5} {'go':>5} keys") for ep in endpoints: ps, pj = get(PY, ep) gs, gj = get(GO, ep) pk, gk = topkeys(pj), topkeys(gj) ok = ps == gs and pk == gk if not ok: failures += 1 print(f"{ep:<36} {str(ps):>5} {str(gs):>5} {'OK' if ok else 'MISMATCH py=%s go=%s' % (pk, gk)}") # Online-overlay endpoints: only structural note (expected to differ for online chars). for ep in (f"/character-stats/{chq}", f"/equipment-cantrip-state/{chq}"): ps, _ = get(PY, ep) gs, _ = get(GO, ep) print(f"{ep:<36} {str(ps):>5} {str(gs):>5} (online live-overlay; exact match only for offline chars)") # Offline-character exact-match check. print("\n-- offline-character exact match (/character-stats, /combat-stats) --") try: online = {p["character_name"] for p in live["players"]} names = subprocess.check_output( ["docker", "exec", "dereth-db", "psql", "-U", "postgres", "-d", "dereth", "-tA", "-c", "SELECT character_name FROM character_stats"], text=True) off = [n for n in names.split("\n") if n.strip() and n not in online] tested = matched = 0 for ch in off: q = urllib.parse.quote(ch) _, py = get(PY, f"/character-stats/{q}") _, go = get(GO, f"/character-stats/{q}") if not (isinstance(py, dict) and len(py.keys()) >= 18): continue tested += 1 same = py == go matched += same if not same: failures += 1 print(f" MISMATCH {ch}: keydiff={set(py) ^ set(go)}") if tested >= 8: break print(f" {matched}/{tested} rich offline chars exact-match") if tested == 0: print(" (no offline rich characters available to test)") except Exception as e: # noqa: BLE001 print(f" (skipped DB-backed offline check: {e})") print("\n" + ("RESULT: read-API parity OK" if failures == 0 else f"RESULT: {failures} mismatch(es)")) return 1 if failures else 0 if __name__ == "__main__": sys.exit(main())