test(market): make the sold A/B one-field attributable, add classified differential

The brief's gate: if the harness varies bidState AND coinsProcessed together, the
client's reaction is attributable to neither. The env knobs were already orthogonal
(--variant and --coins-processed are independent, cp defaults to 0), but
sold-wire-check.py was flipping BOTH for variant B as a convenience, which is exactly
the contaminated A/B the brief forbids. Fixed: the primary pair now holds
coinsProcessed at 0 and asserts the differing-field set is exactly ['bidState'].

New scripts/sold-ab-differential.py is the pre-live gate. It settles ONE synthetic
sale, then re-reads every seller-facing surface under each variant by restarting only
the host (same Core, same DBs, same sale), and diffs with explicit classification --
MISSING / EXTRA / TYPE_MISMATCH / VALUE_MISMATCH -- rather than a boolean "equal?".
Two orthogonal pairs:

  PRIMARY     bidState highest vs buyNow, coinsProcessed held at 0
  ORTHOGONAL  coinsProcessed 0 vs 1,      bidState held at highest

Result, 36/36: the ONLY finding on /tradePile is
VALUE_MISMATCH auctionInfo[0].bidState A='highest' B='buyNow'; /trade/status differs
in exactly the same one path; counts are byte-identical. The orthogonal pair's only
finding is auctionInfo[0].coinsProcessed. C_cp0's sha256 equals A_highest's, so the
capture is reproducible rather than merely consistent.

Counts states the live run has to interpret, measured not guessed:
  S1  0 active + 1 sold -> count 0, selling 0, sold 1
  S2  1 active + 1 sold -> count 1 (active mode) vs 2 (membership mode)
That divergence IS the open question for the client; production is unchanged.

scripts/sold-client-ports.py switches ONLY the two client Blaze port lines, and is
built so restoration cannot depend on memory: it records the production values to a
sidecar on the client BEFORE the first edit and restore reads that sidecar, refusing
if it is absent. It rewrites only known keys (a missing key is an error, never a
silent append), re-reads and verifies afterwards, and REFUSES to edit while a FIFA
client is running because the hook reads the file at connect time.

Phase 0 evidence under docs/evidence/sold-ab-2026-08-18/ with a sha256 per surface,
one file per variant so A can never overwrite B.

Live client A/B NOT run: a production FIFA session is currently live on 10.10.0.105
(pid 32188), and live-session mutual exclusion applies. The client config was NOT
touched -- the switcher's guard refused, as designed.

Production untouched: prod-host pid 3631953, coins 29,843,976, /tradePile 0,
counts.sold 0, club 1966; nothing under /home/alex/openfut-promotion/state/ opened.
This commit is contained in:
funman300
2026-08-18 02:22:12 +00:00
parent 468bc0fba9
commit aa5fb2cc40
9 changed files with 2324 additions and 4 deletions
+7 -4
View File
@@ -299,8 +299,11 @@ def main():
banner("VARIANT B — same state, restart host with closed / buyNow")
host.terminate(); host.wait(timeout=20); procs.remove(host)
# coinsProcessed is held CONSTANT at 0 here: the primary A/B must be
# attributable to bidState alone. The orthogonal coinsProcessed test is a
# separate variant pair (see scripts/sold-ab-differential.py).
host = start_host(tmp, host_port, core_port, host_log, "buyNow",
coins_processed="1", count_mode="active_plus_sold")
coins_processed="0", count_mode="active_plus_sold")
procs.append(host)
st, pileb = req(host_port, "GET", "/ut/game/fifa17/tradePile")
st2, countsb = req(host_port, "GET", "/ut/game/fifa17/tradePile/counts")
@@ -308,13 +311,13 @@ def main():
print(f" bidState={b_row['bidState']} coinsProcessed={b_row['coinsProcessed']} "
f"counts={json.dumps(countsb)}")
check("bidState buyNow (variant B)", b_row.get("bidState") == "buyNow")
check("coinsProcessed 1 when asked", b_row.get("coinsProcessed") == 1)
check("coinsProcessed held constant at 0", b_row.get("coinsProcessed") == 0)
check("count_mode active_plus_sold counts the sold row",
countsb.get("count") == 1 and countsb.get("sold") == 1,
json.dumps(countsb))
differing = sorted(k for k in a_row if a_row.get(k) != b_row.get(k))
check("A/B differ ONLY in bidState and coinsProcessed",
differing == ["bidState", "coinsProcessed"], str(differing))
check("A/B differ ONLY in bidState (one-field attribution)",
differing == ["bidState"], str(differing))
banner("CLEAR — the PE-proven bulk verb DELETE .../trade/sold")
pre_coins = owner_of(core_db, ITEM)[2]