From 8cb2a0f9c65160d6338e8fdf86ffd57c514a1bcc Mon Sep 17 00:00:00 2001 From: funman300 Date: Sat, 22 Aug 2026 00:54:24 +0000 Subject: [PATCH] test(fifa17): smoke-test the apply probe and prove the gate fails closed Unit tests cover classification and body parsing; they do not prove the running host behaves. These three scripts exercise the real service, and they found nothing broken but make the two load-bearing claims checkable: fifa17-apply-snapshot.py Core truth around an apply: coins, owned rows, kind histogram, the source stack's copy count, and the target's mutable fields (contract/fitness/playStyle/ training/injury). Coins and ownership come from the staging DB, not the wire, so the check cannot be satisfied by a projection bug. fifa17-apply-probe-smoke.py Replays the EXACT captured request plus the edges, against the live host, no client needed: 1. {"apply":[{"id":100000003}]} -> 200 {"itemData":[]} source=Consumable subtype=201 copies=1, target=fifa17_200389 rating=87 2. two targets -> 400 apply_batch_unsupported 3. unknown target wire id -> 200 UNRESOLVED_WIRE_ID 4. unowned source -> 200 NOT_OWNED then re-snapshots: Core identical after all four. fifa17-apply-gate-off.py The production-safety claim. With APPLY_PROBE unset the same request must produce the pre-probe behaviour, and does: no apply-probe line, three passthrough lines, 502 into the dead upstream, Core unchanged. Verified by restarting staging without the flag -- an assertion about failing closed is worth nothing unless the closed path is executed. Nothing here writes to production; snapshot reads the staging DB read-only. --- scripts/fifa17-apply-gate-off.py | 41 ++++++++++++++++++ scripts/fifa17-apply-probe-smoke.py | 66 +++++++++++++++++++++++++++++ scripts/fifa17-apply-snapshot.py | 51 ++++++++++++++++++++++ 3 files changed, 158 insertions(+) create mode 100755 scripts/fifa17-apply-gate-off.py create mode 100755 scripts/fifa17-apply-probe-smoke.py create mode 100755 scripts/fifa17-apply-snapshot.py diff --git a/scripts/fifa17-apply-gate-off.py b/scripts/fifa17-apply-gate-off.py new file mode 100755 index 0000000..be9b673 --- /dev/null +++ b/scripts/fifa17-apply-gate-off.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +"""With OPENFUT_FIFA17_APPLY_PROBE unset the apply MUST fall through to the +Python passthrough -- i.e. exactly the behaviour that existed before the probe. +That is the production-safety claim, so prove it rather than assert it.""" +import subprocess +import urllib.error +import urllib.request + +LOG = "/home/alex/openfut-sold-staging/logs/utas-host.log" +mark = sum(1 for _ in open(LOG)) +before = subprocess.run(["python3", "/home/alex/OpenFUT/scripts/fifa17-apply-snapshot.py"], + capture_output=True, text=True).stdout + +req = urllib.request.Request( + "http://127.0.0.1:8299/ut/game/fifa17/item/resource/5001004", + data=b'{"apply":[{"id":100000003}]}', + headers={"X-OpenFUT-Game": "fifa17", "Content-Type": "application/json"}, + method="POST") +try: + with urllib.request.urlopen(req, timeout=30) 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("--- log with the gate OFF ---") +lines = list(open(LOG))[mark:] +for line in lines: + if any(k in line for k in ("apply-probe", "passthrough", "PYTHON")): + print(" " + line.rstrip()[:150]) + +probe_served = any("apply-probe" in line for line in lines) +went_python = any("owner=PYTHON" in line for line in lines) +after = subprocess.run(["python3", "/home/alex/OpenFUT/scripts/fifa17-apply-snapshot.py"], + capture_output=True, text=True).stdout +print("\n probe served (must be False): %s" % probe_served) +print(" proxied to Python (must be True): %s" % went_python) +print(" Core unchanged: %s" % (before == after)) +print("\nRESULT: %s" % ("OK -- gate fails closed" + if (not probe_served and went_python and before == after) + else "FAILED")) diff --git a/scripts/fifa17-apply-probe-smoke.py b/scripts/fifa17-apply-probe-smoke.py new file mode 100755 index 0000000..d9f05d8 --- /dev/null +++ b/scripts/fifa17-apply-probe-smoke.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +"""Smoke-test the consumable-apply probe against the RUNNING staging host. + +Replays the exact request the client sent, plus the batch and unknown-target +edges, then proves Core is byte-identical afterwards. No client needed. +""" +import json +import subprocess +import urllib.error +import urllib.request + +BASE = "http://127.0.0.1:8299" +PATH = "/ut/game/fifa17/item/resource/5001004" +H = {"X-OpenFUT-Game": "fifa17", "Content-Type": "application/json"} +LOG = "/home/alex/openfut-sold-staging/logs/utas-host.log" + + +def post(path, payload): + req = urllib.request.Request( + BASE + path, data=payload, headers=H, method="POST") + try: + with urllib.request.urlopen(req, timeout=30) as r: + return r.status, r.read().decode() + except urllib.error.HTTPError as e: + return e.code, e.read().decode() + + +before = subprocess.run(["python3", "/home/alex/OpenFUT/scripts/fifa17-apply-snapshot.py"], + capture_output=True, text=True).stdout +mark = sum(1 for _ in open(LOG)) + +print("=== 1. the EXACT request captured from the client ===") +st, body = post(PATH, b'{"apply":[{"id":100000003}]}') +print(" status=%s body=%s" % (st, body)) +ok1 = st == 200 and json.loads(body) == {"itemData": []} + +print("\n=== 2. batch: semantics unproven, must be REFUSED not guessed ===") +st2, body2 = post(PATH, b'{"apply":[{"id":100000003},{"id":100000004}]}') +print(" status=%s body=%s" % (st2, body2[:90])) +ok2 = st2 == 400 and "apply_batch_unsupported" in body2 + +print("\n=== 3. unknown target: observed, never invented ===") +st3, body3 = post(PATH, b'{"apply":[{"id":999999999}]}') +print(" status=%s body=%s" % (st3, body3)) +ok3 = st3 == 200 + +print("\n=== 4. unowned source resource ===") +st4, body4 = post("/ut/game/fifa17/item/resource/1234567", b'{"apply":[{"id":100000003}]}') +print(" status=%s body=%s" % (st4, body4)) +ok4 = st4 == 200 + +print("\n=== host log ===") +for line in list(open(LOG))[mark:]: + if "apply-probe" in line: + print(" " + line.rstrip()[:190]) + +after = subprocess.run(["python3", "/home/alex/OpenFUT/scripts/fifa17-apply-snapshot.py"], + capture_output=True, text=True).stdout +same = before == after +print("\n=== 5. Core unchanged by all four requests: %s ===" % ("YES" if same else "NO")) +if not same: + for b, a in zip(before.splitlines(), after.splitlines()): + if b != a: + print(" BEFORE %s\n AFTER %s" % (b.strip(), a.strip())) + +print("\nRESULT: %s" % ("OK" if all([ok1, ok2, ok3, ok4, same]) else "FAILED")) diff --git a/scripts/fifa17-apply-snapshot.py b/scripts/fifa17-apply-snapshot.py new file mode 100755 index 0000000..b3dced5 --- /dev/null +++ b/scripts/fifa17-apply-snapshot.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +"""Core snapshot around the consumable-apply probe: coins, ownership, the source +consumable's copies, and the target's mutable state. Run before and after; the +probe must change NOTHING.""" +import json +import sys +import urllib.request + +H = {"X-OpenFUT-Game": "fifa17"} +SOURCE_RES = 5001004 +TARGET_WIRE = 100000003 + + +def get(p): + req = urllib.request.Request("http://127.0.0.1:8299" + p, headers=H) + with urllib.request.urlopen(req, timeout=30) as r: + return json.loads(r.read()) + + +snap = {} +import sqlite3 +con = sqlite3.connect("file:/home/alex/openfut-sold-staging/staging-core.db?mode=ro", uri=True) +snap["coins"] = con.execute("SELECT coins FROM clubs LIMIT 1").fetchone()[0] +snap["owned_rows"] = con.execute("SELECT COUNT(*) FROM owned_cards").fetchone()[0] +snap["by_kind"] = dict(con.execute("SELECT content_kind, COUNT(*) FROM owned_cards GROUP BY 1")) +con.close() + +cons = get("/ut/game/fifa17/club/consumables/development") +stacks = cons.get("itemData") or [] +snap["development_stacks"] = len(stacks) +snap["development_copies"] = sum(s.get("count", 0) for s in stacks) +snap["source_stack"] = next( + ({"count": s.get("count"), "resourceId": s.get("resourceId")} + for s in stacks if s.get("resourceId") == SOURCE_RES), None) + +sq = get("/ut/game/fifa17/squad/0") +players = (sq.get("squad") or sq).get("players") or [] +for p in players: + it = p.get("itemData") or {} + if it.get("id") == TARGET_WIRE: + snap["target"] = { + "resourceId": it.get("resourceId"), "rating": it.get("rating"), + "contract": it.get("contract"), "fitness": it.get("fitness"), + "injuryType": it.get("injuryType"), "training": it.get("training"), + "playStyle": it.get("playStyle"), "preferredPosition": it.get("preferredPosition"), + } + break + +print(json.dumps(snap, indent=2, sort_keys=True)) +if len(sys.argv) > 1: + open(sys.argv[1], "w").write(json.dumps(snap, sort_keys=True))