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.
This commit is contained in:
@@ -1006,6 +1006,24 @@ fn pure_economy_routes() -> Vec<(&'static str, String, Vec<u8>)> {
|
||||
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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user