tooling: verified backup, state snapshot, retargetable apply validator

Promotion prep for the contract-apply cutover, which unlike the quick-sell
promotion moves BOTH binaries and applies a schema migration.

fifa17-promotion-backup.py uses SQLite's online backup API, not cp. Production
runs WAL with a routinely uncheckpointed WAL (515 KB at capture time); copying
the main file alone is not atomic against a live writer and carries no
guarantee the WAL holds no newer committed state. Emits a checksummed backup, a
metadata record and a RESTORE-*.sh that removes the stale -wal/-shm BEFORE
restoring -- omit that and SQLite replays the old journal over the file you just
put back, resurrecting the state you were abandoning.

fifa17-promotion-snapshot.py is read-only (mode=ro) and counts EVERY table
rather than a hand-picked list, so a delta cannot hide in a table nobody thought
to name. It also fingerprints the ownership rows, catching a row silently
rewritten when counts alone would match.

fifa17-contract-apply-validate.py gains --host/--db so one tool serves staging,
the migration rehearsal and the production acceptance run. Defaults stay
staging: there is deliberately no production default, so a bare invocation
cannot touch production.
This commit is contained in:
funman300
2026-08-22 18:40:12 +00:00
parent 6c97bc4e2b
commit f0c6dcf238
3 changed files with 264 additions and 1 deletions
+12 -1
View File
@@ -11,7 +11,9 @@ Exercises the real route end to end and asserts the full observable contract:
* replaying the exhausted resource fails closed rather than granting again;
* a manager contract and a non-contract consumable both fail closed.
Read-only against production by construction: every URL is the staging port.
Defaults target STAGING. `--host`/`--db` retarget it at a rehearsal or, under
explicit authorization, at the production acceptance run. There is deliberately
no production default: a bare invocation cannot touch production.
"""
import argparse
import json
@@ -20,6 +22,9 @@ import sys
import urllib.error
import urllib.request
# Defaults are STAGING. Override for a rehearsal or the production acceptance
# run; there is deliberately no production default, so a bare invocation can
# never touch production by accident.
HOST = "http://127.0.0.1:8299"
CORE_DB = "/home/alex/openfut-sold-staging/staging-core.db"
HDRS = {"X-OpenFUT-Game": "fifa17"}
@@ -123,7 +128,13 @@ def main():
help="contract resource id to apply (default: first owned player contract)")
ap.add_argument("--target", type=int, default=None,
help="target player wire id (default: lowest-rated owned player)")
ap.add_argument("--host", default=HOST, help=f"utas-host base URL (default {HOST})")
ap.add_argument("--db", default=CORE_DB, help=f"Core SQLite path (default {CORE_DB})")
args = ap.parse_args()
# Rebound before any request so every helper below reads the chosen target.
globals()["HOST"] = args.host
globals()["CORE_DB"] = args.db
print(f"target host : {HOST}\ntarget db : {CORE_DB}\n")
print("== BEFORE ==")
before = core_snapshot()