Adds the rest of the read-side endpoints to the Go tracker, all parity-checked
against the live Python service:
- DB reads: /stats/{c}, /portals, /spawns/heatmap, /server-health,
/character-stats/{c} (stats_data JSONB merged to top level),
/combat-stats[/{c}], /inventories, /inventory/{c}/search.
- 5-minute totals cache + /total-rares, /total-kills.
- Ingest-only state returned as Python's empty/default shapes (/quest-status,
/vital-sharing/peers, /equipment-cantrip-state/{c}); /issues (flat file),
/me (401 until cookie verification lands).
- Streaming reverse proxy to inventory-service (/inventory/{c},
/inventory-characters, /search/*, /sets/list, /inv/{path...} incl. the SSE
suitbuilder stream).
- compare/compare_endpoints.py: structural parity for all read endpoints +
exact-match check for /character-stats and /combat-stats on OFFLINE chars
(online chars legitimately differ — Python serves a richer live overlay that
Phase-1 Go lacks until ingest).
Verified live: 14/14 endpoints structural-match, 8/8 rich offline chars
exact-match on /character-stats.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
110 lines
4.1 KiB
Python
110 lines
4.1 KiB
Python
#!/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())
|