From cf05ab2a9e3cb0e2e1986243c60e1214e8fc42b7 Mon Sep 17 00:00:00 2001 From: OpenFUT Agent Date: Fri, 14 Aug 2026 00:50:03 +0000 Subject: [PATCH] test(fifa17): replay retail purchased/items BUY + route matrix via dispatch - pure_economy_routes (NEVER-BOTH + no-fallback) gains POST/GET purchased/items, lowercase tradepile, tradePile/counts -> all asserted Rust-owned, proxy 0. - retail_purchased_items_buy_debits_core_through_dispatch: the exact round-2 live failure -> POST /purchased/items now debits Core, reveal shows minted items, repeat reveal idempotent, Python proxy count 0. --- .../tests/economy_integration.rs | 109 ++++++++++++++++++ 1 file changed, 109 insertions(+) diff --git a/openfut-utas-host/tests/economy_integration.rs b/openfut-utas-host/tests/economy_integration.rs index cd2a53a..8c0bee1 100644 --- a/openfut-utas-host/tests/economy_integration.rs +++ b/openfut-utas-host/tests/economy_integration.rs @@ -1006,6 +1006,24 @@ fn pure_economy_routes() -> Vec<(&'static str, String, Vec)> { br#"{"packId":70}"#.to_vec(), ), ("GET", "/ut/v2/game/fifa17/purchased".into(), b"".to_vec()), + // ── Round-2 retail shapes: the confirmed BUY uses POST /purchased/items, + // reveal GET /purchased/items; hub tile polls lowercase tradepile + /counts. ── + ( + "POST", + "/ut/game/fifa17/purchased/items".into(), + br#"{"packId":1}"#.to_vec(), + ), + ( + "GET", + "/ut/game/fifa17/purchased/items".into(), + b"".to_vec(), + ), + ("GET", "/ut/game/fifa17/tradepile".into(), b"".to_vec()), + ( + "GET", + "/ut/game/fifa17/tradePile/counts".into(), + b"".to_vec(), + ), ] } @@ -1306,3 +1324,94 @@ async fn retail_v2_store_flow_matches_v1_through_dispatch() { h.abort(); std::fs::remove_dir_all(&dir).ok(); } + +// ─────────────── Retail /purchased/items BUY sequence (round-2 S2 regression) ─ +// +// The rejected candidate 47ced22 sent the confirmed retail Store BUY +// (POST /ut/game/fifa17/purchased/items) to Python and left Core coins unchanged. +// This replays the exact live sequence through real dispatch and asserts the BUY +// debits Core, the reveal shows the minted items, and Python proxy count is 0. + +fn retail_purchased_items_flow(base: &str, dir: &std::path::Path) { + let mock = start_mock_python(); + let (server, client, _r, _s) = build_econ_server(base, dir, &mock.url); + let calls0 = mock.calls.load(std::sync::atomic::Ordering::SeqCst); + + // Store screen catalogue (v1 /all) — Rust. + let pg = server.handle_with_ip( + "GET", + "/ut/game/fifa17/store/purchasegroup/all", + &[], + b"", + None, + ); + assert_eq!(pg.status, 200, "purchasegroup/all Rust-owned"); + + // THE CONFIRMED RETAIL BUY: POST /ut/game/fifa17/purchased/items must debit Core. + let bal0 = client.balance().unwrap(); + let buy = server.handle_with_ip( + "POST", + "/ut/game/fifa17/purchased/items", + &[], + br#"{"packId":1}"#, + None, + ); + assert_eq!(buy.status, 200, "purchased/items BUY handled by Rust (200)"); + assert!( + !buy.body.windows(10).any(|w| w == b"__python__"), + "purchased/items BUY is Rust-owned (no Python marker)" + ); + let bal1 = client.balance().unwrap(); + assert!( + bal1 < bal0, + "purchased/items BUY debited Core ({bal0} -> {bal1}) — the round-2 S2 was NO debit" + ); + + // Reveal poll: GET /ut/game/fifa17/purchased/items — Rust, shows the minted items. + let reveal = server.handle_with_ip("GET", "/ut/game/fifa17/purchased/items", &[], b"", None); + assert_eq!(reveal.status, 200, "purchased/items reveal Rust-owned"); + let rv: Value = serde_json::from_slice(&reveal.body).unwrap(); + let revealed = rv["itemData"].as_array().map_or(0, |a| a.len()); + assert!(revealed > 0, "reveal shows the freshly-minted items: {rv}"); + + // Repeat reveal is idempotent (no re-grant, no extra debit). + let bal2 = client.balance().unwrap(); + let _ = server.handle_with_ip("GET", "/ut/game/fifa17/purchased/items", &[], b"", None); + assert_eq!( + client.balance().unwrap(), + bal2, + "repeat reveal does not mutate coins" + ); + + // NEVER any Python proxy across the whole /purchased/items sequence. + assert_eq!( + mock.calls.load(std::sync::atomic::Ordering::SeqCst), + calls0, + "retail /purchased/items sequence never reached the Python proxy" + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn retail_purchased_items_buy_debits_core_through_dispatch() { + let dir = std::env::temp_dir().join(format!( + "openfut-econ-pi-{}-{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos() + )); + std::fs::create_dir_all(&dir).unwrap(); + let db_url = format!("sqlite://{}/econ.db", dir.display()); + let (h, base) = start_core_seeded(&db_url, true).await; + let (b, d) = (base.clone(), dir.clone()); + tokio::task::spawn_blocking(move || { + std::thread::spawn(move || retail_purchased_items_flow(&b, &d)) + .join() + .expect("purchased/items flow thread") + }) + .await + .expect("purchased/items phase"); + h.abort(); + std::fs::remove_dir_all(&dir).ok(); +}