fix(fifa17): map market resource ids to authoritative Core card ids
Closes the market correctness gap: handle_market_list recorded listing.card_id from the raw FIFA wire resourceId, so a synthetic buy minted a card_id Core could not resolve — it survived the immediate response but Core's content preflight rejected it on reboot. - catalog.rs: keep the by_resource reverse index (was built then discarded) and expose `card_id_for_resource(resource_id) -> Option<&str>` — exact reverse of the card_id->asset catalog, no heuristics, unknown => None. - lib.rs: `impl MarketCardResolver for Fifa17IdentityResolver` delegates to the same catalog /club shaping uses; Core never sees a FIFA resource id. - market_store.rs: listings now carry BOTH `card_id` (authoritative Core content, what a buy MINTS) and `wire_resource_id` (the FIFA wire id, echoed in the auction record). New column; create_listing takes both; row/Listing updated. - market.rs: `MarketCardResolver` trait; handle_market_list resolves resourceId -> Core card_id and fails closed (persists nothing) on an unmappable resource; auction_record emits `resourceId` from wire_resource_id. Dispatch passes the resolver. Tests: list_unknown_resource_fails_closed_no_listing (B), list_persists_core_card_and_wire_resource_across_reopen (C), catalog reverse lookup; and the dispatch E2E now RESTORES the full Core+store restart (economy_full_sequence_through_dispatch_and_restart) — the synthetic buy mints a real reverse-mapped card_id, so Core's content preflight passes on reboot (A+D). market 23 lib + catalog 15 + 2 integration green; clippy -D warnings + fmt clean.
This commit is contained in:
@@ -259,8 +259,9 @@ async fn economy_end_to_end_and_restart_persistence() {
|
||||
// sequence runs on a plain OS thread (no ambient Tokio runtime), exactly like the
|
||||
// thread-per-connection server, so the bridge takes its DIRECT `block_on` path.
|
||||
|
||||
/// Facts captured from the write sequence, re-checked after reopening the stores.
|
||||
/// Facts captured from the write sequence, re-checked after a full restart.
|
||||
struct SeqResult {
|
||||
final_balance: i64,
|
||||
sold_listing: String,
|
||||
moved_core_id: String,
|
||||
}
|
||||
@@ -273,7 +274,7 @@ struct SeqResult {
|
||||
fn build_econ_server(
|
||||
base: &str,
|
||||
dir: &std::path::Path,
|
||||
) -> (Server, HttpCoreClient, Arc<Fifa17IdentityResolver>) {
|
||||
) -> (Server, HttpCoreClient, Arc<Fifa17IdentityResolver>, i64) {
|
||||
let probe = HttpCoreClient::new(base, "fifa17");
|
||||
let owned = probe.all_owned().expect("core collection");
|
||||
assert!(!owned.is_empty(), "seed must grant a starter collection");
|
||||
@@ -337,7 +338,9 @@ fn build_econ_server(
|
||||
33068179,
|
||||
)
|
||||
.with_economy(services);
|
||||
(server, probe, resolver)
|
||||
// asset 20000 is assigned to the first distinct seeded definition, so wire
|
||||
// resourceId 20000 reverse-maps to a real Core card_id (a valid synthetic mint).
|
||||
(server, probe, resolver, 20000)
|
||||
}
|
||||
|
||||
/// Drive the whole writer+reader cluster through the real dispatch. Panics on any
|
||||
@@ -346,7 +349,7 @@ fn economy_sequence(base: &str, dir: &std::path::Path) -> SeqResult {
|
||||
wait_ready(base);
|
||||
// Core is seeded (start_core_seeded): a fifa17 profile with 100k coins + one
|
||||
// owned instance per definition. No /auth/local — the profile already exists.
|
||||
let (server, client, resolver) = build_econ_server(base, dir);
|
||||
let (server, client, resolver, sample_resource) = build_econ_server(base, dir);
|
||||
let start = client.balance().unwrap();
|
||||
assert!(start >= 5000, "seeded dev balance present ({start})");
|
||||
|
||||
@@ -440,7 +443,10 @@ fn economy_sequence(base: &str, dir: &std::path::Path) -> SeqResult {
|
||||
"POST",
|
||||
"/ut/game/fifa17/auctionhouse",
|
||||
&[],
|
||||
br#"{"itemData":{"id":777,"resourceId":777},"buyNowPrice":1000,"startingBid":500}"#,
|
||||
format!(
|
||||
r#"{{"itemData":{{"id":777,"resourceId":{sample_resource}}},"buyNowPrice":1000,"startingBid":500}}"#
|
||||
)
|
||||
.as_bytes(),
|
||||
None,
|
||||
)
|
||||
.expect("market list routed");
|
||||
@@ -501,7 +507,10 @@ fn economy_sequence(base: &str, dir: &std::path::Path) -> SeqResult {
|
||||
"POST",
|
||||
"/ut/game/fifa17/auctionhouse",
|
||||
&[],
|
||||
br#"{"itemData":{"id":888,"resourceId":888},"buyNowPrice":1000,"startingBid":500}"#,
|
||||
format!(
|
||||
r#"{{"itemData":{{"id":888,"resourceId":{sample_resource}}},"buyNowPrice":1000,"startingBid":500}}"#
|
||||
)
|
||||
.as_bytes(),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
@@ -559,17 +568,25 @@ fn economy_sequence(base: &str, dir: &std::path::Path) -> SeqResult {
|
||||
.expect("moved item reverses to a Core id");
|
||||
|
||||
SeqResult {
|
||||
final_balance: client.balance().unwrap(),
|
||||
sold_listing: trade_id.to_string(),
|
||||
moved_core_id,
|
||||
}
|
||||
}
|
||||
|
||||
/// Reopen the durable market/pile SQLite stores from the SAME files (a fresh
|
||||
/// process would do exactly this) and prove the sold listing and the pile move
|
||||
/// persisted. Core-side coin/inventory persistence across a full Core restart is
|
||||
/// proven by `economy_end_to_end_and_restart_persistence`; here the focus is the
|
||||
/// host-owned durable stores.
|
||||
fn verify_store_durability(dir: &std::path::Path, seq: &SeqResult) {
|
||||
/// After a FULL restart from the SAME on-disk state — Core rebooted from its
|
||||
/// SQLite file, and the durable market/pile stores reopened from their files —
|
||||
/// coins, the sold listing, and the pile move all persist. The synthetic market
|
||||
/// buy now mints a REAL Core `card_id` (resourceId reverse-mapped), so Core's
|
||||
/// content preflight passes on reboot.
|
||||
fn verify_economy_restart(base: &str, dir: &std::path::Path, seq: &SeqResult) {
|
||||
wait_ready(base);
|
||||
let client = HttpCoreClient::new(base, "fifa17");
|
||||
assert_eq!(
|
||||
client.balance().unwrap(),
|
||||
seq.final_balance,
|
||||
"coins persisted across Core restart"
|
||||
);
|
||||
let bridge = AsyncBridge::new().unwrap();
|
||||
let market_path = dir.join("market.db").to_string_lossy().into_owned();
|
||||
let market = bridge
|
||||
@@ -598,7 +615,7 @@ fn verify_store_durability(dir: &std::path::Path, seq: &SeqResult) {
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
||||
async fn economy_full_sequence_through_dispatch() {
|
||||
async fn economy_full_sequence_through_dispatch_and_restart() {
|
||||
let dir = std::env::temp_dir().join(format!(
|
||||
"openfut-econ-dispatch-{}-{}",
|
||||
std::process::id(),
|
||||
@@ -622,18 +639,18 @@ async fn economy_full_sequence_through_dispatch() {
|
||||
.expect("write sequence");
|
||||
h1.abort();
|
||||
|
||||
// Durable host stores: reopen the market/pile files from disk (as a fresh
|
||||
// process would) and prove sold/pile state persisted. No Core rebuild — the
|
||||
// synthetic market mint uses the wire resourceId as a placeholder card id,
|
||||
// which Core's content preflight (correctly) rejects on reboot; Core-side
|
||||
// coin/inventory restart persistence is covered by the sibling test.
|
||||
let d2 = dir.clone();
|
||||
// Core #2: same on-disk Core DB + same market/pile files — prove full restart
|
||||
// persistence (coins + sold listing + pile). Core content preflight passes
|
||||
// because the synthetic mint used a real reverse-mapped card_id.
|
||||
let (h2, base2) = start_core_seeded(&db_url, false).await;
|
||||
let (b2, d2) = (base2.clone(), dir.clone());
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let t = std::thread::spawn(move || verify_store_durability(&d2, &seq));
|
||||
t.join().expect("durability thread")
|
||||
let t = std::thread::spawn(move || verify_economy_restart(&b2, &d2, &seq));
|
||||
t.join().expect("restart thread")
|
||||
})
|
||||
.await
|
||||
.expect("durability phase");
|
||||
.expect("restart phase");
|
||||
h2.abort();
|
||||
|
||||
std::fs::remove_dir_all(&dir).ok();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user