Files
OpenFUT/scripts/fifa17-migration-rehearse.py
T
funman300 88b4cad780 test(fifa17): migration invariant capture and rehearsal harness
fifa17-migration-invariants.py  Pre/post invariants across every domain the
    migration authorization names: coins, ownership (+kind histogram, distinct
    definitions, chemistry styles, loans, position overrides), squads,
    managers, staff, consumables, club items, transfer state (market_listings),
    packs, SBC, match history, plus integrity_check and foreign_key_check.
    Table names are the REAL schema, not guessed: transfer state lives in
    market_listings (29 rows in production), the FIFA17 opaque squad blob in
    game_entity_ext.

fifa17-migration-rehearse.py    Serves a migrated COPY with the candidate Rust
    stack on isolated ports and validates the wire surface: club discardValue
    is table-derived, squad projects, consumable categories populate, and the
    apply probe is OFF (502 upstream-unavailable rather than a diagnostic ack).

Both are read-only against production: the rehearsal operates on a copy under
/home/alex/openfut-migration/, and nothing under openfut-promotion/state is
opened.

Evidence from the 2026-08-22 rehearsal is written up in the Vault runbook
"FIFA17 Rust Production Migration (rehearsed)".
2026-08-22 01:15:45 +00:00

105 lines
3.9 KiB
Python
Executable File

#!/usr/bin/env python3
"""Serve the MIGRATED + RECLASSIFIED rehearsal copy with the candidate Rust
stack and validate the wire surface. Isolated ports, copy-only state, no
production or staging resource touched."""
import json
import os
import signal
import subprocess
import time
import urllib.error
import urllib.request
R = "/home/alex/openfut-migration/rehearsal-20260822"
REPO = "/home/alex/OpenFUT"
CORE_PORT, HOST_PORT = 18099, 18098
H = {"X-OpenFUT-Game": "fifa17"}
procs = []
def start(name, binary, env):
e = dict(os.environ)
e.update(env)
log = open("%s/evidence/%s.log" % (R, name), "w")
p = subprocess.Popen([binary], cwd=REPO, env=e, stdout=log, stderr=log,
start_new_session=False)
procs.append(p)
return p
def get(port, path):
req = urllib.request.Request("http://127.0.0.1:%d%s" % (port, path), headers=H)
with urllib.request.urlopen(req, timeout=25) as r:
return json.loads(r.read())
try:
start("rehearsal-core", R + "/artifacts/openfut-core", {
"LISTEN_ADDR": "127.0.0.1:%d" % CORE_PORT,
"DATABASE_URL": "sqlite://%s/work/core.db" % R,
"DATA_DIR": "%s/openfut-core/data" % REPO,
"OPENFUT_CONTENT_PACKS": "%s/emit/content/fifa17-production-cards.json" % R,
"RUST_LOG": "warn",
})
time.sleep(6)
start("rehearsal-host", R + "/artifacts/openfut-utas-host", {
"OPENFUT_UTAS_HOST_ADDR": "127.0.0.1:%d" % HOST_PORT,
"OPENFUT_UTAS_PYTHON_URL": "http://127.0.0.1:19999", # deliberately dead
"OPENFUT_CORE_URL": "http://127.0.0.1:%d" % CORE_PORT,
"OPENFUT_FIFA17_TABLES_DIR": "%s/fifa17-recon/data/tables" % REPO,
"OPENFUT_FIFA17_CATALOG": "%s/emit/content/fifa17-production-catalog.json" % R,
"OPENFUT_IDENTITY_STORE": "%s/work/identity.json" % R,
"OPENFUT_PERSONA_ID": "33068179",
"OPENFUT_MARKET_DB": "%s/work/market.db" % R,
"OPENFUT_PILE_DB": "%s/work/pile.db" % R,
"OPENFUT_FIFA17_DISCARD_TABLE": "1",
"RUST_LOG": "warn",
})
time.sleep(6)
checks = {}
club = get(HOST_PORT, "/ut/game/fifa17/club?type=player&start=0&count=5")
checks["club_total"] = club.get("totalResults")
checks["club_first_discard"] = (club.get("itemData") or [{}])[0].get("discardValue")
checks["club_first_rating"] = (club.get("itemData") or [{}])[0].get("rating")
umi = get(HOST_PORT, "/ut/game/fifa17/userMassInfo")
checks["squad_slots"] = len(((umi.get("userInfo") or {}).get("squad") or
umi.get("squad") or {}).get("players", []) or [])
for cat in ("contracts", "development", "fitness"):
d = get(HOST_PORT, "/ut/game/fifa17/club/consumables/" + cat)
st = d.get("itemData") or []
checks["consumables_" + cat] = (len(st), sum(s.get("count", 0) for s in st))
sq = get(HOST_PORT, "/ut/game/fifa17/squad/0")
checks["squad_players"] = len((sq.get("squad") or sq).get("players") or [])
print(json.dumps(checks, indent=2, sort_keys=True))
print("\n=== apply probe MUST be off in the candidate ===")
req = urllib.request.Request(
"http://127.0.0.1:%d/ut/game/fifa17/item/resource/5001004" % HOST_PORT,
data=b'{"apply":[{"id":100000003}]}',
headers={"X-OpenFUT-Game": "fifa17", "Content-Type": "application/json"},
method="POST")
try:
with urllib.request.urlopen(req, timeout=20) as r:
st, body = r.status, r.read().decode()
except urllib.error.HTTPError as e:
st, body = e.code, e.read().decode()
print(" status=%s body=%s" % (st, body))
print(" probe OFF (must be 502 upstream-unavailable): %s"
% (st == 502 and "upstream" in body))
finally:
for p in procs:
try:
os.kill(p.pid, signal.SIGTERM)
except ProcessLookupError:
pass
time.sleep(2)
for p in procs:
if p.poll() is None:
os.kill(p.pid, signal.SIGKILL)
print("\nrehearsal processes stopped")