feat(fifa17): route SBCs through atomic Rust Core

This commit is contained in:
funman300
2026-08-18 18:26:35 +00:00
parent bf6db98f0d
commit f9740f640d
8 changed files with 1345 additions and 7 deletions
@@ -409,6 +409,24 @@ fn pack_ids(pg: &Value) -> Vec<u64> {
v
}
/// Preserve every object key, array position, and JSON scalar kind while discarding
/// wire-insignificant values such as translated labels and live completion counts.
fn json_shape(value: &Value) -> Value {
match value {
Value::Null => json!("null"),
Value::Bool(_) => json!("bool"),
Value::Number(_) => json!("number"),
Value::String(_) => json!("string"),
Value::Array(values) => Value::Array(values.iter().map(json_shape).collect()),
Value::Object(values) => Value::Object(
values
.iter()
.map(|(key, value)| (key.clone(), json_shape(value)))
.collect(),
),
}
}
/// Every op runs on a plain OS thread with NO ambient Tokio runtime (the blocking
/// Core client + `reqwest::blocking` require this), exactly like the
/// thread-per-connection server — the bridge takes its direct `block_on` path.
@@ -1240,6 +1258,130 @@ fn run_differential(core_base: &str, oracle: &Oracle, dir: &std::path::Path) {
assert_eq!(matrix.len(), 16, "all economy ops classified");
}
/// Compare the complete reversed `/sbs/*` response family against the Python oracle.
/// Listing labels and counters deliberately come from Core, so parity is defined as the
/// exact key/container/scalar-kind graph. Submission is the one semantic deviation:
/// Python acknowledges any body without consuming cards; Rust rejects an empty squad.
fn run_sbc_differential(core_base: &str, oracle: &Oracle, dir: &std::path::Path) {
wait_ready(core_base);
let (server, _client, _sample_resource) = build_econ_server(core_base, dir);
let cases = [
("GET", "/ut/game/fifa17/sbs/sets", None),
("GET", "/ut/game/fifa17/sbs/setId/1/challenges", None),
("GET", "/ut/game/fifa17/sbs/setId/2/challenges", None),
("GET", "/ut/game/fifa17/sbs/setId/999/challenges", None),
("POST", "/ut/game/fifa17/sbs/sets/tag", Some(json!({}))),
("POST", "/ut/game/fifa17/sbs/challenge/101", None),
("GET", "/ut/game/fifa17/sbs/challenge/101/squad", None),
(
"PUT",
"/ut/game/fifa17/sbs/challenge/101/squad",
Some(json!({ "squad": [] })),
),
];
for (method, path, body) in cases {
let oracle_result = oracle.req(method, path, body.clone(), None);
let bytes = body
.as_ref()
.map(|value| serde_json::to_vec(value).unwrap())
.unwrap_or_default();
let rust_result = rust(&server, method, path, &bytes, None);
assert_eq!(
rust_result.0, oracle_result.0,
"{method} {path} status parity"
);
assert_eq!(
json_shape(&rust_result.1),
json_shape(&oracle_result.1),
"{method} {path} wire shape parity\nRust: {}\nOracle: {}",
rust_result.1,
oracle_result.1
);
match (method, path) {
("GET", "/ut/game/fifa17/sbs/sets") => {
assert_eq!(rust_result.1["categories"][0]["categoryId"], 1);
assert_eq!(oracle_result.1["categories"][0]["categoryId"], 1);
let rust_set_ids: Vec<i64> = rust_result.1["categories"][0]["sets"]
.as_array()
.unwrap()
.iter()
.map(|set| set["setId"].as_i64().unwrap())
.collect();
let oracle_set_ids: Vec<i64> = oracle_result.1["categories"][0]["sets"]
.as_array()
.unwrap()
.iter()
.map(|set| set["setId"].as_i64().unwrap())
.collect();
assert_eq!(rust_set_ids, [1, 2]);
assert_eq!(oracle_set_ids, [1, 2]);
}
("GET", "/ut/game/fifa17/sbs/setId/1/challenges") => {
for body in [&rust_result.1, &oracle_result.1] {
assert_eq!(body["challenges"][0]["challengeId"], 101);
assert_eq!(body["challenges"][0]["setId"], 1);
assert_eq!(body["challenges"][0]["categoryId"], 1);
}
}
("GET", "/ut/game/fifa17/sbs/setId/2/challenges") => {
for body in [&rust_result.1, &oracle_result.1] {
assert_eq!(body["challenges"][0]["challengeId"], 201);
assert_eq!(body["challenges"][0]["setId"], 2);
assert_eq!(body["challenges"][0]["categoryId"], 1);
}
}
("GET", "/ut/game/fifa17/sbs/setId/999/challenges") => {
assert_eq!(rust_result.1["challenges"], json!([]));
assert_eq!(oracle_result.1["challenges"], json!([]));
}
("POST", "/ut/game/fifa17/sbs/challenge/101") => {
assert_eq!(rust_result.1["challengeId"], 101);
assert_eq!(oracle_result.1["challengeId"], 101);
}
(method, "/ut/game/fifa17/sbs/challenge/101/squad") => {
assert!(method == "GET" || method == "PUT");
assert_eq!(rust_result.1["id"], 101);
assert_eq!(oracle_result.1["id"], 101);
}
_ => {}
}
}
let submit = json!({ "squad": [] });
let oracle_submit = oracle.req(
"PUT",
"/ut/game/fifa17/sbs/challenge/101",
Some(submit.clone()),
None,
);
let rust_submit = rust(
&server,
"PUT",
"/ut/game/fifa17/sbs/challenge/101",
&serde_json::to_vec(&submit).unwrap(),
None,
);
assert_eq!(oracle_submit.0, 200, "oracle preserves its no-op submit");
assert_eq!(
json_shape(&oracle_submit.1),
json_shape(&json!({
"challengeId": 0,
"setId": 0,
"credits": 0,
"preOrderPacks": 0,
"recoveredPacks": 0,
"grantedChallengeAwards": [],
"grantedSetAwards": []
})),
"oracle submit response remains freeze-safe"
);
assert_eq!(
rust_submit.0, 400,
"Rust must validate instead of copying the oracle's no-op acceptance"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn economy_differential_python_oracle() {
let dir = std::env::temp_dir().join(format!(
@@ -1281,3 +1423,35 @@ async fn economy_differential_python_oracle() {
std::fs::remove_dir_all(&dir).ok();
outcome.expect("differential thread panicked");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn sbc_differential_python_oracle() {
let dir = std::env::temp_dir().join(format!(
"openfut-sbc-diff-{}-{}",
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://{}/sbc.db", dir.display());
let (core_handle, core_base) = start_core_seeded(&db_url, true).await;
let core_base_run = core_base.clone();
let dir_run = dir.clone();
let outcome = tokio::task::spawn_blocking(move || {
std::thread::spawn(move || {
let mut oracle = Oracle::spawn(&dir_run);
oracle.wait_ready();
run_sbc_differential(&core_base_run, &oracle, &dir_run);
})
.join()
})
.await
.expect("join spawn_blocking");
core_handle.abort();
std::fs::remove_dir_all(&dir).ok();
outcome.expect("SBC differential thread panicked");
}