test(fifa17): prove host economy concurrency and failure rollback

Two real host-dispatch test files (no fakes) driving Server::try_handle_economy
against a live in-process Core over the real blocking client + durable
MarketStore/PileStore + JsonIdentityStore, each racer its own OS thread
(off-runtime pattern).

economy_concurrency.rs — 8 races x 50 iterations:
  A two BUYs (coins for one) -> exactly one 200 + one 461, final 0, one debit.
  B duplicate owned-pack open -> one redemption, +11 once, entitlement once.
  C duplicate quick-sell -> one sell + one credit + one removal.
  D two market buyers -> one win, one debit, one mint, sold once.
  E reward+BUY -> no lost update (Core relative UPDATE under BEGIN IMMEDIATE).
  F move+quick-sell / G list+quick-sell -> one coherent transition.
  H 1000 concurrent mints -> unique + reversible wire ids, monotonic watermark.

economy_failure.rs — 10 fault-injection sub-cases, all fail-closed:
  BUY/open-redeem/generator/pile/identity, quick-sell, move, market
  reserve/purchase/complete. CRITICAL complete-sale-after-commit = SAFE: the
  listing is left `reserved` (not active), so the active->reserved reserve CAS
  can never win again -> not buyable, exactly one debit + one mint. No E3.

Fault injection uses test-file CoreEconomy/ExternalIdentityStore doubles plus a
NARROW, inert-by-default `StoreFault` seam in market_store.rs + pile_store.rs
(the concrete stores have no trait boundary; 3 `tripped()` checks + a field,
zero behaviour unless a test arms it). `parking_lot` promoted to a normal dep
(the seam's Mutex is used at lib scope). Classifier/ROUTE_AUTHORITY/Python
untouched. host lib 71/71; both new tests pass.
This commit is contained in:
OpenFUT Agent
2026-08-13 22:30:35 +00:00
parent 43917a0051
commit b1643309f6
5 changed files with 1659 additions and 2 deletions
+14 -1
View File
@@ -55,6 +55,7 @@ fn now_millis() -> String {
#[derive(Clone)]
pub struct PileStore {
pool: SqlitePool,
fault: crate::market_store::StoreFault,
}
impl PileStore {
@@ -84,7 +85,16 @@ impl PileStore {
.execute(&pool)
.await
.map_err(db)?;
Ok(PileStore { pool })
Ok(PileStore {
pool,
fault: crate::market_store::StoreFault::default(),
})
}
/// A shared handle to this store's test-only fault switch (inert unless a
/// test arms it). Production never calls it.
pub fn fault(&self) -> crate::market_store::StoreFault {
self.fault.clone()
}
/// The current pile of a Core-owned item, or `None` if none is recorded.
@@ -100,6 +110,9 @@ impl PileStore {
/// Set (upsert) the pile of a Core-owned item. Durable and race-safe
/// (`BEGIN IMMEDIATE` + upsert).
pub async fn set(&self, core_item_id: &str, pile: &str) -> Result<(), PileError> {
if self.fault.tripped("set") {
return Err(PileError::Db("injected pile set fault".into()));
}
let updated_at = now_millis();
let mut conn = self.pool.acquire().await.map_err(db)?;
sqlx::query("BEGIN IMMEDIATE")