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:
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user