fifa17 store: real 6-pack economy + full-DB pool; drop extPrice

- store_catalog: replace the invented catalogue with the real always-available
  FUT17 regular packs (Bronze/Prem Bronze/Silver/Prem Silver/Gold/Prem Gold) at
  real prices + tier composition; PackDef now carries per-tier quantities.
- pack_body: drop extPrice (its mtx side-effect switched on the broken "or %1s"
  FIFA-Points tile line; plan-2026-08-05-store-subsystem.md section 3.4).
- pack_content: tier-aware generator draws each pack bronze/silver/gold
  composition with special_chance bias + empty-tier fallback.
- host: CoreAccess::all_definitions (GET /cards); build_content_pool draws the
  FULL card universe via non-minting catalog lookup, owned-inventory fallback.
- economy_differential: store ops reclassified DIFFERENT-BY-DESIGN (Rust is the
  authoritative store; Python oracle stays the untouched rollback baseline).
- fixtures/tests updated to the real catalogue.

Odds are DESIGNED placeholders (FUT17 pack probabilities were never published);
club items remain excluded (cardtype-9 mapping unknown). Full regression green;
real prices + tier-correct draws verified server-side on staging.
This commit is contained in:
funman300
2026-08-18 23:26:55 +00:00
parent 7116046195
commit c68c10cf04
10 changed files with 863 additions and 650 deletions
+71 -49
View File
@@ -7,20 +7,16 @@
//! (only card ids that resolve in BOTH the FIFA catalogue and Core content),
//! mints the drawn cards into Core, and shapes them onto the wire.
//!
//! ## Parity note — Python `open_pack` / `_pack_body`
//! (`fifa17-recon/tools/fut_store.py:689`, `utas_server.py:3474`)
//! 1. `open_pack(price, count, gold, tiers, special_chance)` deducts coins then
//! draws `count` items (mostly players); the reveal body wraps them verbatim.
//! 2. Non-tiered draws split the pool at rating 75 by `gold` (`p[1] >= 75 == gold`)
//! and fall back to the whole pool when that tier is empty (`... or PACK_POOL`).
//! 3. Each drawn player becomes a special with probability `special_chance`
//! (`random.random() < special_chance`).
//! 4. `FUT_PACK_MIX` swaps ~`count // 4` players for consumables/staff extras;
//! we deliberately OMIT that mix (Core candidates are player defs — players-only).
//! 5. Prices/counts/odds are the OpenFUT **PLACEHOLDER** economy (the audit found
//! them invented); only the wire *shape* is EA-observed/oracle-verified.
//! 6. This port reproduces the count + gold-tier split + `special_chance` gate as
//! that same PLACEHOLDER policy, drawing with replacement from the pool.
//! ## Policy (real FUT 17 composition, DESIGNED odds)
//!
//! A pack draws [`PackDef::count`] cards split across rating tiers by the pack's
//! `n_bronze`/`n_silver`/`n_gold` composition (the same numbers the tile shows in
//! `packContentInfo`), drawing with replacement from the candidate pool. Each pick
//! is biased toward a special version with probability `special_chance` (a DESIGNED
//! placeholder — FUT 17 pack odds are unrecoverable). An empty tier falls back to
//! the whole pool so a draw is always possible even when the pool lacks that tier.
//! `FUT_PACK_MIX` (consumable/staff extras) is deliberately OMITTED — Core
//! candidates are player defs.
use rand::Rng;
@@ -77,50 +73,67 @@ pub struct GeneratedCard {
pub attributes: [u8; 6],
}
/// Draw `pack.count` cards from `pool` with the injected RNG. Pure and
/// deterministic under a seeded RNG. Returns an empty `Vec` (fail-closed) when
/// the pool is empty or the pack awards no cards.
/// Draw a pack's cards from `pool` with the injected RNG. Pure and deterministic
/// under a seeded RNG. Returns an empty `Vec` (fail-closed) when the pool is empty
/// or the pack awards no cards.
///
/// Policy (PLACEHOLDER — see the module parity note): draw with replacement from
/// the pack's tier (`gold`), biasing each draw toward a special card with
/// probability `special_chance`. An empty tier or partition falls back to the
/// next-wider set so a draw is always possible when the pool is non-empty.
/// Draws the pack's per-tier composition (`n_gold` gold-tier, `n_silver` silver,
/// `n_bronze` bronze), biasing each pick toward a special with `special_chance`.
/// An empty tier falls back to the whole pool (so a draw is always possible).
pub fn generate_pack_contents(
pack: &PackDef,
rng: &mut impl Rng,
pool: &[GeneratedCandidate],
) -> Vec<GeneratedCard> {
if pool.is_empty() || pack.count == 0 {
if pool.is_empty() || pack.count() == 0 {
return Vec::new();
}
// Tier split: a gold pack draws gold-tier candidates, a non-gold pack draws
// non-gold; an empty tier falls back to the whole pool (oracle `... or POOL`).
let tier: Vec<&GeneratedCandidate> = pool.iter().filter(|c| c.gold == pack.gold).collect();
let tier: Vec<&GeneratedCandidate> = if tier.is_empty() {
pool.iter().collect()
} else {
tier
};
// Partition the tier by special so `special_chance` can bias a draw; either
// partition falls back to the whole tier when empty.
let special: Vec<&GeneratedCandidate> = tier.iter().copied().filter(|c| c.special).collect();
let normal: Vec<&GeneratedCandidate> = tier.iter().copied().filter(|c| !c.special).collect();
let chance = pack.special_chance.clamp(0.0, 1.0);
let all: Vec<&GeneratedCandidate> = pool.iter().collect();
let gold: Vec<&GeneratedCandidate> = pool.iter().filter(|c| c.rating >= 75).collect();
let silver: Vec<&GeneratedCandidate> =
pool.iter().filter(|c| (65..75).contains(&c.rating)).collect();
let bronze: Vec<&GeneratedCandidate> = pool.iter().filter(|c| c.rating < 65).collect();
let mut out = Vec::with_capacity(pack.count as usize);
for _ in 0..pack.count {
let mut out = Vec::with_capacity(pack.count() as usize);
draw_tier(&mut out, &gold, &all, pack.n_gold, pack.special_chance, rng);
draw_tier(&mut out, &silver, &all, pack.n_silver, pack.special_chance, rng);
draw_tier(&mut out, &bronze, &all, pack.n_bronze, pack.special_chance, rng);
out
}
/// Draw `n` cards from `tier` — or the whole-pool `fallback` when `tier` is empty —
/// biasing each pick toward a special version with probability `chance`. Either the
/// special or normal partition falls back to the tier when empty.
fn draw_tier(
out: &mut Vec<GeneratedCard>,
tier: &[&GeneratedCandidate],
fallback: &[&GeneratedCandidate],
n: u64,
chance: f64,
rng: &mut impl Rng,
) {
if n == 0 {
return;
}
let src: &[&GeneratedCandidate] = if tier.is_empty() { fallback } else { tier };
if src.is_empty() {
return;
}
let special: Vec<&GeneratedCandidate> = src.iter().copied().filter(|c| c.special).collect();
let normal: Vec<&GeneratedCandidate> = src.iter().copied().filter(|c| !c.special).collect();
let chance = chance.clamp(0.0, 1.0);
for _ in 0..n {
let want_special = chance > 0.0 && rng.gen_bool(chance);
let sub: &[&GeneratedCandidate] = if want_special && !special.is_empty() {
&special
} else if !want_special && !normal.is_empty() {
&normal
} else {
&tier
src
};
let pick = sub[rng.gen_range(0..sub.len())];
out.push(pick.to_card());
}
out
}
#[cfg(test)]
@@ -156,13 +169,22 @@ mod tests {
]
}
fn pack(id: u64, count: u64, gold: bool, special_chance: f64) -> PackDef {
fn pack(id: u64, n_bronze: u64, n_silver: u64, n_gold: u64, special_chance: f64) -> PackDef {
PackDef {
id,
name: "Test Pack",
price: 1000,
count,
gold,
n_bronze,
n_silver,
n_gold,
rares: 0,
category: if n_gold > 0 {
"gold"
} else if n_silver > 0 {
"silver"
} else {
"bronze"
},
special_chance,
owned_only: false,
}
@@ -171,7 +193,7 @@ mod tests {
#[test]
fn same_seed_same_output() {
let pool = pool();
let p = pack(5, 7, true, 0.3);
let p = pack(5, 0, 0, 7, 0.3);
let mut a = StdRng::seed_from_u64(42);
let mut b = StdRng::seed_from_u64(42);
assert_eq!(
@@ -183,7 +205,7 @@ mod tests {
#[test]
fn different_seeds_can_diverge() {
let pool = pool();
let p = pack(5, 7, true, 0.3);
let p = pack(5, 0, 0, 7, 0.3);
let a = generate_pack_contents(&p, &mut StdRng::seed_from_u64(1), &pool);
let b = generate_pack_contents(&p, &mut StdRng::seed_from_u64(999), &pool);
// Not a hard guarantee, but with this pool/count the two seeds differ.
@@ -196,7 +218,7 @@ mod tests {
let ids: std::collections::HashSet<&str> =
pool.iter().map(|c| c.card_id.as_str()).collect();
for &n in &[1u64, 5, 7, 11] {
let p = pack(6, n, true, 0.08);
let p = pack(6, 0, 0, n, 0.08);
let cards = generate_pack_contents(&p, &mut StdRng::seed_from_u64(n), &pool);
assert_eq!(cards.len() as u64, n);
for c in &cards {
@@ -212,7 +234,7 @@ mod tests {
#[test]
fn gold_pack_draws_only_gold_tier() {
let pool = pool();
let p = pack(5, 20, true, 0.03);
let p = pack(5, 0, 0, 20, 0.03);
let cards = generate_pack_contents(&p, &mut StdRng::seed_from_u64(7), &pool);
assert!(
cards.iter().all(|c| c.rating >= 75),
@@ -223,7 +245,7 @@ mod tests {
#[test]
fn bronze_pack_draws_only_bronze_tier() {
let pool = pool();
let p = pack(1, 20, false, 0.005);
let p = pack(1, 20, 0, 0, 0.005);
let cards = generate_pack_contents(&p, &mut StdRng::seed_from_u64(7), &pool);
assert!(
cards.iter().all(|c| c.rating < 75),
@@ -239,7 +261,7 @@ mod tests {
.filter(|c| c.special)
.map(|c| c.card_id.as_str())
.collect();
let p = pack(7, 11, true, 1.0);
let p = pack(7, 0, 0, 11, 1.0);
let cards = generate_pack_contents(&p, &mut StdRng::seed_from_u64(3), &pool);
assert!(cards
.iter()
@@ -248,7 +270,7 @@ mod tests {
#[test]
fn empty_pool_fails_closed() {
let p = pack(5, 7, true, 0.03);
let p = pack(5, 0, 0, 7, 0.03);
assert!(generate_pack_contents(&p, &mut StdRng::seed_from_u64(1), &[]).is_empty());
}
}
+118 -106
View File
@@ -1,95 +1,97 @@
//! FIFA 17 Store pack catalogue + `/store/purchasegroup` wire shaping.
//!
//! A faithful Rust port of the Python oracle's `PACK_CATALOG` + `_pack_body` +
//! `store_catalog` assembly (`fifa17-recon/tools/{fut_store,utas_server}.py`) at the
//! **production flag defaults** (`FUT_STORE_DISPLAYGROUP=1` on, `FUT_STORE_GROUPID=0`
//! off, `FUT_PRICE_PROBE=0` off). Parity is pinned by differential fixtures generated
//! from the Python oracle (`tests/fixtures/purchasegroup_*.json`).
//! Rust is the authoritative store owner: [`build_purchasegroup`] is served live
//! by the host via `EconomyRoute::PurchaseGroup` over Core economy authority (Core
//! owns coins + unopened packs), so there is no Python dependency and no
//! dual-write/split-brain.
//!
//! ## Scope / split-brain safety
//! ## Relationship to the Python oracle
//!
//! This is **pure wire shaping** — no economy state, no IO. [`build_purchasegroup`]
//! is a function of `(owned unopened pack ids, empty-My-Packs StoreMode)`. It is
//! deliberately **not yet wired** into the live host: serving purchasegroup from Rust
//! requires an authoritative Rust owner of `unopenedPackIds`, and today Python is the
//! single writer of coins + unopened packs (BUY, quick-sell, rewards). Wiring this
//! before that economy authority exists would create a dual-write/split-brain. See
//! the R3 economy-authority prerequisite in the vault (`Rust UTAS Migration`).
//! The wire *shape* was RE'd from the client and cross-checked against the Python
//! oracle's `_pack_body`/`store_catalog`
//! (`fifa17-recon/tools/{fut_store,utas_server}.py`). Rust now diverges from the
//! oracle where the RE proved the oracle wrong: it does NOT emit `extPrice`, whose
//! parser side-effect creates an `"mtx"` currency row and switches on the broken
//! `or %1s` FIFA-Points tile line (plan-2026-08-05-store-subsystem.md §3.4). The
//! Python oracle stays the rollback baseline and is never modified; the
//! `tests/fixtures/purchasegroup_*.json` goldens pin Rust's authoritative output.
//!
//! ## Economy-parameter provenance
//!
//! Prices, counts and odds are the current OpenFUT **PLACEHOLDER** economy, NOT
//! EA-authentic (the overnight audit established the store economy is invented). The
//! wire *shape* is EA-observed/oracle-verified; the *numbers* are placeholders.
//! Pack prices and tier composition are the real always-available FUT 17
//! regular-store packs (community-documented on fifauteam). Pack ODDS
//! (`special_chance`) are DESIGNED placeholders, NOT EA-authentic — EA never
//! published FUT 17 pack probabilities. The wire *shape* is EA-observed/RE-verified.
use serde_json::{json, Value};
use crate::fut::store_session::{StoreMode, SENTINEL_PACK_ID};
/// A FIFA 17 Store pack definition. Wire shape is oracle-verified; the economy
/// numbers (`price`/`count`/`special_chance`) are OpenFUT PLACEHOLDER, not EA-authentic.
/// A FIFA 17 Store pack definition. The wire *shape* is RE-verified; the price and
/// per-tier composition are the real always-available FUT 17 regular-store packs,
/// with DESIGNED (not EA-authentic) `special_chance` odds.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct PackDef {
pub id: u64,
pub name: &'static str,
pub price: u64,
pub count: u64,
pub gold: bool,
/// Cards awarded per rating-tier band: bronze `< 65`, silver `65..=74`, gold
/// `>= 75`. These are ALSO the wire `packContentInfo` per-tier quantities, so a
/// pack's displayed composition matches what its generator draws.
pub n_bronze: u64,
pub n_silver: u64,
pub n_gold: u64,
/// `rareQuantity` shown on the tile (wire display only).
pub rares: u64,
/// StoreFront category token (`displayGroup.value`): one of the six hard-coded
/// client tokens — here `"bronze"`, `"silver"` or `"gold"`.
pub category: &'static str,
/// Per-draw probability the awarded card is a special version (DESIGNED
/// placeholder; FUT 17 odds are unrecoverable).
pub special_chance: f64,
/// Reward-only pack (no purchase path): excluded from the normal catalogue,
/// rendered only when owned (in `unopenedPackIds`).
pub owned_only: bool,
}
/// The current supported FIFA 17 pack catalogue (`fut_store.py:820`). Only observed/
/// currently-supported ids. The 65534 sentinel is deliberately ABSENT — it is a
/// compatibility shim, never a catalogue pack (never purchasable/openable).
impl PackDef {
/// Total cards awarded / wire `itemQuantity` — the sum of the per-tier counts.
pub fn count(&self) -> u64 {
self.n_bronze + self.n_silver + self.n_gold
}
}
/// The always-available FIFA 17 FUT regular-store packs (real fifauteam-documented
/// prices + tier composition), plus the OpenFUT reward pack. Two packs per client
/// category (bronze/silver/gold), which the client renders as separate buyable tiles
/// on drill-in. The 65534 sentinel is deliberately ABSENT — a compatibility shim,
/// never purchasable/openable.
pub const PACK_CATALOG: &[PackDef] = &[
PackDef {
id: 1,
name: "Bronze Pack",
price: 400,
count: 5,
gold: false,
special_chance: 0.005,
owned_only: false,
},
PackDef {
id: 5,
name: "Gold Pack",
price: 5000,
count: 7,
gold: true,
special_chance: 0.03,
owned_only: false,
},
PackDef {
id: 6,
name: "Premium Gold",
price: 15000,
count: 11,
gold: true,
special_chance: 0.08,
owned_only: false,
},
PackDef {
id: 7,
name: "Special Players Pack",
price: 25000,
count: 11,
gold: true,
special_chance: 1.0,
owned_only: false,
},
PackDef {
id: 70,
name: "Reward Special Players Pack",
price: 0,
count: 11,
gold: true,
special_chance: 1.0,
owned_only: true,
},
// ── Bronze category ──
PackDef { id: 1, name: "Bronze Pack", price: 400,
n_bronze: 10, n_silver: 2, n_gold: 0, rares: 1, category: "bronze",
special_chance: 0.01, owned_only: false },
PackDef { id: 2, name: "Premium Bronze Pack", price: 750,
n_bronze: 10, n_silver: 2, n_gold: 0, rares: 3, category: "bronze",
special_chance: 0.02, owned_only: false },
// ── Silver category ──
PackDef { id: 3, name: "Silver Pack", price: 2500,
n_bronze: 1, n_silver: 11, n_gold: 0, rares: 1, category: "silver",
special_chance: 0.015, owned_only: false },
PackDef { id: 4, name: "Premium Silver Pack", price: 3750,
n_bronze: 1, n_silver: 11, n_gold: 0, rares: 3, category: "silver",
special_chance: 0.03, owned_only: false },
// ── Gold category ──
PackDef { id: 5, name: "Gold Pack", price: 5000,
n_bronze: 0, n_silver: 2, n_gold: 10, rares: 1, category: "gold",
special_chance: 0.04, owned_only: false },
PackDef { id: 6, name: "Premium Gold Pack", price: 7500,
n_bronze: 0, n_silver: 2, n_gold: 10, rares: 3, category: "gold",
special_chance: 0.06, owned_only: false },
// ── Reward (owned-only; opened from My Packs, never coin-purchasable) ──
PackDef { id: 70, name: "Reward Gold Pack", price: 0,
n_bronze: 0, n_silver: 0, n_gold: 11, rares: 11, category: "gold",
special_chance: 1.0, owned_only: true },
];
/// Look up a catalogue pack by id (the 65534 sentinel is never present).
@@ -97,27 +99,27 @@ pub fn pack_by_id(id: u64) -> Option<&'static PackDef> {
PACK_CATALOG.iter().find(|p| p.id == id)
}
/// The FIFA17 StoreFront category token for a NORMAL pack tile (`utas_server.py:3579`):
/// one of the six hard-coded tokens the client resolves.
/// The FIFA 17 StoreFront category token for a pack tile (`displayGroup.value`):
/// one of the six hard-coded tokens the client resolves. Each catalogue pack
/// carries its own token; owned/reward packs take `mypacks` instead (see
/// [`pack_body`]).
fn category(p: &PackDef) -> &'static str {
if p.special_chance >= 1.0 {
"special"
} else if p.gold {
"gold"
} else {
"bronze"
}
p.category
}
/// One `purchase[]` entry — the faithful `_pack_body` port (`utas_server.py:3474`) at
/// production flag defaults. `owned` packs (My Packs / reward / sentinel) drop the
/// purchase fields and take the `mypacks` display group.
pub fn pack_body(p: &PackDef, idx: u64, owned: bool) -> Value {
let mtx = std::cmp::max(1, p.price / 100);
let pack_type = match p.category {
"gold" => "GOLD",
"silver" => "SILVER",
_ => "BRONZE",
};
let mut body = json!({
"assetId": p.id,
"id": p.id,
"packType": if p.gold { "GOLD" } else { "BRONZE" },
"packType": pack_type,
"description": p.name,
"state": "active",
"saleType": "promo",
@@ -127,26 +129,32 @@ pub fn pack_body(p: &PackDef, idx: u64, owned: bool) -> Value {
"purchaseCount": 0,
"isPremium": false,
"sortPriority": idx,
// The tile renders `finalFunds` as its coin price; the HUD balance reads
// `funds` from the /credits currencies array. We keep the pair equal.
//
// NO `extPrice`. Its parser has a SIDE EFFECT: `finalPrice`/`originalPrice`
// both CREATE an `"mtx"` currency row, which the tile adapter reads as "has
// a real-money price" and switches on the broken `or %1s` label — the
// Origin/Dime commerce catalogue that would fill it no longer exists
// offline, so every string stays at its constructor default. Omitting the
// key is the documented fix (plan-2026-08-05-store-subsystem.md §3.4 /
// experiment #4): it strictly reduces executed client code and leaves every
// tile buyable. LIVE-observed `or %1s` on the Store tiles, 2026-08-18.
"currencies": [{ "name": "coins", "funds": p.price, "finalFunds": p.price }],
"extPrice": {
"finalPrice": { "amount": mtx, "currency": "mtx" },
"originalPrice": { "amount": mtx, "currency": "mtx" },
},
"packContentInfo": {
"bronzeQuantity": if p.gold { 0 } else { p.count },
"silverQuantity": 0,
"goldQuantity": if p.gold { p.count } else { 0 },
"rareQuantity": if p.gold { p.count } else { 0 },
"itemQuantity": p.count,
"bronzeQuantity": p.n_bronze,
"silverQuantity": p.n_silver,
"goldQuantity": p.n_gold,
"rareQuantity": p.rares,
"itemQuantity": p.count(),
},
"unopened": owned,
});
let obj = body.as_object_mut().expect("pack body is a JSON object");
if owned {
// Reward/My-Packs tiles have no purchase path; leaving zero-value coin/mtx
// objects makes the client render the price label as literal "undefined".
// Reward/My-Packs tiles have no purchase path; drop the coin row so the
// client never formats an unavailable payment label as literal "undefined".
obj.remove("currencies");
obj.remove("extPrice");
obj.insert(
"displayGroup".into(),
json!({ "value": "mypacks", "priority": idx }),
@@ -165,8 +173,11 @@ pub fn sentinel_body(idx: u64) -> Value {
id: SENTINEL_PACK_ID,
name: "",
price: 0,
count: 0,
gold: true,
n_bronze: 0,
n_silver: 0,
n_gold: 0,
rares: 0,
category: "gold",
special_chance: 0.0,
owned_only: true,
};
@@ -179,10 +190,10 @@ pub fn sentinel_body(idx: u64) -> Value {
body
}
/// Build the full `/store/purchasegroup` body from the authoritative unopened-pack ids
/// and the frozen empty-My-Packs mode. Pure — mirrors `store_catalog` (`3627`):
/// normal packs (1,5,6,7) first, then any owned packs, then the empty-My-Packs shim
/// (sentinel for [`StoreMode::Sentinel`], nothing for [`StoreMode::CleanV1`]).
/// Build the full `/store/purchasegroup` body from the authoritative unopened-pack
/// ids and the frozen empty-My-Packs mode. Pure: the six regular packs (ids 16)
/// first, then any owned packs, then the empty-My-Packs shim (sentinel for
/// [`StoreMode::Sentinel`], nothing for [`StoreMode::CleanV1`]).
pub fn build_purchasegroup(unopened_ids: &[u64], mode: StoreMode) -> Value {
let mut packs: Vec<Value> = PACK_CATALOG
.iter()
@@ -203,10 +214,11 @@ pub fn build_purchasegroup(unopened_ids: &[u64], mode: StoreMode) -> Value {
#[cfg(test)]
mod tests {
//! Differential parity against the Python oracle. The fixtures under
//! `tests/fixtures/purchasegroup_*.json` are generated by calling the oracle's
//! `_pack_body`/`store_catalog` at production flag defaults; Rust must match
//! them semantically (object key order is irrelevant to `serde_json::Value` eq).
//! Golden tests pinning the authoritative Rust `/store/purchasegroup` body. The
//! fixtures under `tests/fixtures/purchasegroup_*.json` are Rust's own output
//! (object key order is irrelevant to `serde_json::Value` eq). They track the
//! RE-driven divergence from the Python oracle — notably no `extPrice` (see the
//! module header).
use super::*;
fn parse(s: &str) -> Value {
@@ -214,7 +226,7 @@ mod tests {
}
#[test]
fn purchasegroup_zero_sentinel_matches_oracle() {
fn purchasegroup_zero_sentinel_matches_golden() {
let got = build_purchasegroup(&[], StoreMode::Sentinel);
let want = parse(include_str!(
"../../tests/fixtures/purchasegroup_zero_sentinel.json"
@@ -223,7 +235,7 @@ mod tests {
}
#[test]
fn purchasegroup_zero_clean_matches_oracle() {
fn purchasegroup_zero_clean_matches_golden() {
let got = build_purchasegroup(&[], StoreMode::CleanV1);
let want = parse(include_str!(
"../../tests/fixtures/purchasegroup_zero_clean.json"
@@ -232,7 +244,7 @@ mod tests {
}
#[test]
fn purchasegroup_pack70_matches_oracle() {
fn purchasegroup_pack70_matches_golden() {
// Owned pack present -> no sentinel regardless of mode.
let got = build_purchasegroup(&[70], StoreMode::Sentinel);
let want = parse(include_str!(
@@ -256,13 +268,13 @@ mod tests {
.iter()
.map(|e| e["id"].as_u64().unwrap())
.collect();
assert_eq!(ids, vec![1, 5, 6, 7]);
assert_eq!(ids, vec![1, 2, 3, 4, 5, 6]);
}
#[test]
fn category_tokens_are_canonical() {
assert_eq!(category(pack_by_id(1).unwrap()), "bronze");
assert_eq!(category(pack_by_id(3).unwrap()), "silver");
assert_eq!(category(pack_by_id(5).unwrap()), "gold");
assert_eq!(category(pack_by_id(7).unwrap()), "special");
}
}