fifa17-recon: decouple contract suite from the Python implementation
test_fut_contract.py no longer imports ACCOUNT from fut_account. The expected persona comes from FUT_TEST_PERSONA_ID and the target from FUT_TEST_BASE, so the suite now imports nothing but stdlib and talks to a server at a URL. That is what lets these 380 checks certify ANY implementation of the reversed spec, a future Rust openfut-core included, without replaying the reverse engineering. The original reason for reading ACCOUNT still holds and is preserved in the comment: suite and server must not each hold a private copy of the constant, or the identity-consistency checks would only prove two copies matched. Also adds pileSizeClientData(0x227) behind FUT_PILESIZES (default off). A probe run with 16 uniquely-valued entries did NOT move the MY CLUB counter, so that member is eliminated as its source; the code is kept for the record and flagged off. Docs: OPENFUT_PROJECT_REPORT.md and OPENFUT_HANDOFF.md. The report now separates "built but untested" from "never requested by the client" -- the server log records User-Agent, and splitting real client traffic (ProtoHttp) from this project's own probes shows /season, /tournament, /champion, /match, /clubUser and /user/list are at ZERO client requests. /clubUser (0 client, 93 probe) and /user/list (0 client, 180 probe) are the starkest: work was done on both assuming the client wanted them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW
This commit is contained in:
@@ -20,16 +20,26 @@ Exit 0 = all pass. No pytest dependency (stdlib only).
|
||||
"""
|
||||
import json, os, sys, urllib.error, urllib.request
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from fut_account import ACCOUNT # the identity every layer must agree on
|
||||
|
||||
BASE = "http://127.0.0.1:8099"
|
||||
BASE = os.environ.get("FUT_TEST_BASE", "http://127.0.0.1:8099")
|
||||
G = "/ut/game/fifa17"
|
||||
V2 = "/ut/v2/game/fifa17"
|
||||
# No PERSONA_ID literal here any more. This suite and the server MUST read the
|
||||
# same source or the "identity is consistent" checks below would only be proving
|
||||
# that two copies of a constant were copied correctly.
|
||||
PERSONA_ID = ACCOUNT.persona_id
|
||||
|
||||
# IMPLEMENTATION-INDEPENDENT BY CONSTRUCTION.
|
||||
# This suite talks to a server at a URL and imports NOTHING from the server's own
|
||||
# code. That is what lets it verify ANY implementation of the reversed spec -- a
|
||||
# future Rust openfut-core included -- without replaying the reverse engineering.
|
||||
#
|
||||
# It used to do `from fut_account import ACCOUNT` for the persona, which was a
|
||||
# Python import against the Python implementation and quietly made the suite
|
||||
# unable to certify a non-Python server. The expected persona now comes from the
|
||||
# environment, defaulting to the value every layer has agreed on all along.
|
||||
#
|
||||
# The original reason for reading ACCOUNT still stands and is preserved: the suite
|
||||
# and the server must not each hold their own copy of the constant, or the
|
||||
# "identity is consistent" checks would only prove that two copies were copied
|
||||
# correctly. Point FUT_TEST_PERSONA_ID at whatever the server under test is
|
||||
# configured with; the default matches the shipped default.
|
||||
PERSONA_ID = int(os.environ.get("FUT_TEST_PERSONA_ID", "33068179"))
|
||||
|
||||
_fail = []
|
||||
_pass = 0
|
||||
|
||||
@@ -499,6 +499,45 @@ SETTINGS = {"configs": []}
|
||||
_MI = os.environ.get("FUT_MASSINFO", "full")
|
||||
|
||||
|
||||
# ---- pileSizeClientData: the MY CLUB counter --------------------------------
|
||||
# LIVE EVIDENCE (2026-08-04): the user opened MY CLUB, the client fetched GET /club
|
||||
# and DISPLAYED all 99 players -- and the MY CLUB counter still read 0. So that
|
||||
# counter is NOT derived from the item list; it is a PILE SIZE, delivered
|
||||
# separately. massinfo's pileSizeClientData(0x227) is that member and we have never
|
||||
# sent it. Parser 0x18013adb0: {"entries":[{"key":<int>,"value":<int>}]} -- key and
|
||||
# value BOTH read with the int getter 0x1801c79d0, and the parser IS skip-safe.
|
||||
#
|
||||
# The pile-id enum is not recoverable from the strings (the "club"/"tradepile"
|
||||
# literals are just atom names in the alphabetical key table). So rather than guess:
|
||||
#
|
||||
# FUT_PILESIZES=probe -> emit one entry per candidate key 0..15 with a UNIQUE
|
||||
# recognisable value (100+key). Whatever number MY CLUB then displays names the
|
||||
# club pile's key: 103 means key 3. One launch identifies the enum.
|
||||
# FUT_PILESIZES=1 -> emit the REAL counts once PILE_KEY_CLUB below is known.
|
||||
#
|
||||
# Default OFF: this adds a member to boot-critical massinfo. It is a documented
|
||||
# member of that parser and carries only ints, so the risk is low -- but "low" is
|
||||
# what I said about displayGroup before it froze the store, so it ships behind a flag.
|
||||
_PILESIZES = os.environ.get("FUT_PILESIZES", "")
|
||||
PILE_KEY_CLUB = int(os.environ.get("FUT_PILE_KEY_CLUB", "-1")) # set once probed
|
||||
|
||||
|
||||
def pile_size_body():
|
||||
"""massinfo.pileSizeClientData -- see the note above."""
|
||||
if _PILESIZES == "probe":
|
||||
return {"entries": [{"key": k, "value": 100 + k} for k in range(16)]}
|
||||
counts = {
|
||||
"club": len(STORE.items()),
|
||||
"purchased": len(STORE.purchased()),
|
||||
"tradepile": len(STORE.listings()),
|
||||
}
|
||||
if PILE_KEY_CLUB >= 0:
|
||||
return {"entries": [{"key": PILE_KEY_CLUB, "value": counts["club"]}]}
|
||||
# No verified key yet -> announce the club count on every candidate key. Crude,
|
||||
# but every value is truthful, so no pile can be told a wrong number.
|
||||
return {"entries": [{"key": k, "value": counts["club"]} for k in range(16)]}
|
||||
|
||||
|
||||
def massinfo():
|
||||
if _MI == "empty":
|
||||
return {} # old known-hub-reaching body
|
||||
@@ -518,6 +557,10 @@ def massinfo():
|
||||
# body and adding a member to it is the change class behind the last two
|
||||
# live regressions. Instant fallback: FUT_MASSINFO=squad.
|
||||
body["clubUser"] = club_user_body()
|
||||
if _PILESIZES:
|
||||
# pileSizeClientData(0x227) -> parser 0x18013adb0, int-only, skip-safe.
|
||||
# This is what the MY CLUB counter reads (see pile_size_body above).
|
||||
body["pileSizeClientData"] = pile_size_body()
|
||||
return body
|
||||
|
||||
# ---- FUT item-definition serving (wf_e41070d8) -------------------------------
|
||||
|
||||
Reference in New Issue
Block a user