fifa17-recon: contract test guarding the move-verdict shape (392 checks)

The bug we just closed was invisible to every existing check: PUT /item answered 200
with a well-formed JSON body, and the body told the client the move had FAILED. Seven
attempts, weeks of investigation, and nothing in the suite would have noticed a
regression back to {}.

test_move_verdict_shape asserts the record vector exists, has ONE RECORD PER REQUESTED
ITEM (an empty vector fails the client exactly as hard as a wrong field), and that
id/pile/success carry the number/string/bool types their getters require.

Non-mutating: it asks to move ids that cannot exist, so nothing changes pile. The
verdicts come back success=false, which is honest and is not what is asserted.

Verified it actually catches the regression rather than just passing:
  default (ack)        392 checks passed, 0 failed
  FUT_MOVE_BODY=empty  382 passed, 1 FAILED -- "returns itemData array"

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW
This commit is contained in:
funman300
2026-08-04 11:32:04 -07:00
parent dd8dddd7af
commit b7943aead3
+47 -1
View File
@@ -436,12 +436,58 @@ def test_club_rename_roundtrip():
repr((ui.get("clubName"), ui.get("clubAbbr"))))
def test_move_verdict_shape():
"""PUT /item must return per-item VERDICT records. This guards the fix for the
project's longest-lived bug.
FutMoveCard's deserializer does not parse an acknowledgement, it builds a vector
of verdict records, and the completion handler raises
EVENT_CARDS_MOVE_CARD_FAILURE when that vector is EMPTY or when a record's
success byte (record+0x0c) is not 1. For seven attempts this endpoint answered
{}, an echo of the moved cards, or a dreamSquads stub, and every one of them told
the client the move had failed, so the client killed the FUT session. It looked
like a client-state bug for weeks because the HTTP status was always 200.
That failure mode is silent at the transport layer, which is exactly why it needs
a contract test: nothing else in this suite would notice a regression to {}.
NON-MUTATING. It asks to move ids that cannot exist, so no item changes pile. The
verdicts therefore come back success=false, which is the honest answer and is not
what is being asserted here. What is asserted is the SHAPE and the RECORD COUNT,
because an empty vector fails the client just as hard as a wrong flag.
"""
ghosts = [{"id": 999000001, "pile": "club", "swap": 0, "tradeId": 0},
{"id": 999000002, "pile": "club", "swap": 0, "tradeId": 0}]
code, body = _req("PUT", G + "/item", {"itemData": ghosts})
check("PUT /item -> 200", code == 200, repr(code))
check("PUT /item body is an object", is_obj(body), repr(body))
recs = body.get("itemData") if is_obj(body) else None
check("PUT /item returns itemData array (NOT {} -- {} reads as move-failed)",
is_arr(recs), repr(body)[:120])
if not is_arr(recs):
return
check("PUT /item returns one record per requested item",
len(recs) == len(ghosts), "%d records for %d items" % (len(recs), len(ghosts)))
for i, r in enumerate(recs):
check("record[%d] is an object" % i, is_obj(r), repr(r))
if not is_obj(r):
continue
# id(0x15c) INT via 0x1801c79d0 -- a string here desyncs the reader
check("record[%d].id is a number" % i, is_num(r.get("id")), repr(r.get("id")))
# pile(0x226) STRING via 0x1801c7aa0 -> enum 0x180142650
check("record[%d].pile is a string" % i, is_str(r.get("pile")), repr(r.get("pile")))
# success(0x2fa) BOOL via 0x1801c7620 -> record+0x0c
check("record[%d].success is a bool" % i, isinstance(r.get("success"), bool),
repr(r.get("success")))
def main():
tests = [test_credits, test_v2_store_gate, test_store_catalog, test_market_bodies,
test_auction_record_shape, test_squad_boot, test_squad_list_shape,
test_squad_list_endpoint, test_massinfo_shape, test_club_items,
test_identity_consistency, test_club_user_shape, test_club_info_shape,
test_accountinfo_shape, test_club_rename_roundtrip]
test_accountinfo_shape, test_club_rename_roundtrip,
test_move_verdict_shape]
try:
_get(G + "/user/credits")
except Exception as e: