MosswartOverlord/go-services/compare/compare_endpoints.py
Erik c4e8190656 feat(go-services): tracker-go — complete the Phase 1 read API
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>
2026-06-24 09:38:10 +02:00

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