fix(fifa17): classify full retail economy route shapes
Round-2 live staging (candidate 47ced22) showed the CONFIRMED retail Store BUY
uses POST /ut/game/fifa17/purchased/items (reveal GET .../purchased/items),
which the exact-tail 'purchased' match missed -> Python (Core coins unchanged).
Comprehensive audited fix in classify_economy:
- is_purchased_tail: 'purchased' AND 'purchased/items' (POST->PackOpen,
GET->PackReveal); bounded (rejects purchasedfoo, purchased/items/extra).
- is_tradepile_tail: 'tradePile' family CASE-INSENSITIVE incl 'tradePile/counts'
(hub tile polls lowercase; oracle routes via re.I); allocation-free.
- (kept) v1/v2 prefix normalization + store/transaction[/<digits>].
Adds retail_route_matrix unit test = the machine-auditable route contract gate
(all economy shapes + negative near-misses). Host lib 75.
This commit is contained in:
@@ -231,6 +231,28 @@ fn is_store_transaction_tail(tail: &str) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
/// `purchased` or `purchased/items` — pack OPEN (POST) / reveal (GET). Retail
|
||||
/// sends the `/items` sub-path (FutPurchaseItemsServerResponse); the Python
|
||||
/// oracle's bare `/purchased` regex matches both. Bounded to exactly these two
|
||||
/// tails (rejects `purchasedfoo`, `purchased/items/extra`).
|
||||
fn is_purchased_tail(tail: &str) -> bool {
|
||||
tail == "purchased" || tail == "purchased/items"
|
||||
}
|
||||
|
||||
/// `tradePile` or `tradePile/counts` — the user's own listings (query) + the
|
||||
/// listing-count tile. CASE-INSENSITIVE: the FUT hub tile polls lowercase
|
||||
/// `tradepile`/`tradepile/counts` while the screen uses camelCase `tradePile`
|
||||
/// (the oracle routes both via `re.I`). Bounded to the `tradepile` family
|
||||
/// (base tail or a `tradepile/<sub>` path); allocation-free.
|
||||
fn is_tradepile_tail(tail: &str) -> bool {
|
||||
const BASE: &str = "tradePile";
|
||||
match tail.len() {
|
||||
9 => tail.eq_ignore_ascii_case(BASE),
|
||||
n if n > 9 => tail.as_bytes()[9] == b'/' && tail[..9].eq_ignore_ascii_case(BASE),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Classify a FIFA17 economy route from method + path, mirroring the Python
|
||||
/// oracle's route table (`utas_server.py` §1418-1553). Returns `None` for any
|
||||
/// non-economy path. Path is already query-stripped by the caller.
|
||||
@@ -263,12 +285,12 @@ pub fn classify_economy(method: &str, path: &str) -> Option<EconomyRoute> {
|
||||
Some("user/credits") if get => Some(EconomyRoute::Credits),
|
||||
Some(t) if get && t.starts_with("store/purchasegroup") => Some(EconomyRoute::PurchaseGroup),
|
||||
Some(t) if put && is_store_transaction_tail(t) => Some(EconomyRoute::StoreBuy),
|
||||
Some("purchased") if post => Some(EconomyRoute::PackOpen),
|
||||
Some("purchased") if get => Some(EconomyRoute::PackReveal),
|
||||
Some(t) if post && is_purchased_tail(t) => Some(EconomyRoute::PackOpen),
|
||||
Some(t) if get && is_purchased_tail(t) => Some(EconomyRoute::PackReveal),
|
||||
Some(t) if delete && is_item_id_tail(t) => Some(EconomyRoute::QuickSellPath),
|
||||
Some("item") if put => Some(EconomyRoute::MoveItems),
|
||||
Some(t) if (t == "auctionhouse" || t == "transfermarket") => Some(EconomyRoute::MarketList),
|
||||
Some("tradePile") if get => Some(EconomyRoute::MarketQuery),
|
||||
Some(t) if get && is_tradepile_tail(t) => Some(EconomyRoute::MarketQuery),
|
||||
Some(t) if t.starts_with("trade") => Some(EconomyRoute::MarketBuy),
|
||||
_ => None,
|
||||
}
|
||||
@@ -3247,4 +3269,93 @@ mod tests {
|
||||
assert_eq!(classify_economy("GET", "/ut/v2/game/fifa17/store"), None);
|
||||
assert_eq!(classify_economy("POST", "/ut/auth"), None);
|
||||
}
|
||||
|
||||
/// RETAIL_ROUTE_MATRIX — the audited retail economy route contract. Every
|
||||
/// economy row MUST classify to its Rust route (Python proxy forbidden);
|
||||
/// every negative near-miss MUST stay `None` (proxied). Permanent gate against
|
||||
/// "Python knows route X, Rust forgot route X".
|
||||
#[test]
|
||||
fn retail_route_matrix() {
|
||||
use EconomyRoute::*;
|
||||
let matrix: &[(&str, &str, Option<EconomyRoute>)] = &[
|
||||
// credits
|
||||
("GET", "/ut/game/fifa17/user/credits", Some(Credits)),
|
||||
// purchasegroup (v1 + v2 + /all)
|
||||
(
|
||||
"GET",
|
||||
"/ut/game/fifa17/store/purchasegroup",
|
||||
Some(PurchaseGroup),
|
||||
),
|
||||
(
|
||||
"GET",
|
||||
"/ut/game/fifa17/store/purchasegroup/all",
|
||||
Some(PurchaseGroup),
|
||||
),
|
||||
(
|
||||
"GET",
|
||||
"/ut/v2/game/fifa17/store/purchasegroup/all",
|
||||
Some(PurchaseGroup),
|
||||
),
|
||||
// store transaction (v2 + trailing id) — round-1 fix
|
||||
("PUT", "/ut/game/fifa17/store/transaction", Some(StoreBuy)),
|
||||
(
|
||||
"PUT",
|
||||
"/ut/v2/game/fifa17/store/transaction/0",
|
||||
Some(StoreBuy),
|
||||
),
|
||||
// purchased + purchased/items — round-2 fix (POST open, GET reveal)
|
||||
("POST", "/ut/game/fifa17/purchased", Some(PackOpen)),
|
||||
("POST", "/ut/game/fifa17/purchased/items", Some(PackOpen)),
|
||||
("POST", "/ut/v2/game/fifa17/purchased/items", Some(PackOpen)),
|
||||
("GET", "/ut/game/fifa17/purchased", Some(PackReveal)),
|
||||
("GET", "/ut/game/fifa17/purchased/items", Some(PackReveal)),
|
||||
// move
|
||||
("PUT", "/ut/game/fifa17/item", Some(MoveItems)),
|
||||
// quick-sell (path + body)
|
||||
(
|
||||
"DELETE",
|
||||
"/ut/game/fifa17/item/100000001",
|
||||
Some(QuickSellPath),
|
||||
),
|
||||
("POST", "/ut/delete/game/fifa17/item", Some(QuickSellBody)),
|
||||
(
|
||||
"POST",
|
||||
"/ut/v2/delete/game/fifa17/item",
|
||||
Some(QuickSellBody),
|
||||
),
|
||||
// match end
|
||||
("POST", "/ut/delete/game/fifa17/match", Some(MatchEnd)),
|
||||
// market list / query (case-insensitive tradePile + counts) / buy / cancel
|
||||
("POST", "/ut/game/fifa17/auctionhouse", Some(MarketList)),
|
||||
("POST", "/ut/game/fifa17/transfermarket", Some(MarketList)),
|
||||
("GET", "/ut/game/fifa17/tradePile", Some(MarketQuery)),
|
||||
("GET", "/ut/game/fifa17/tradepile", Some(MarketQuery)),
|
||||
("GET", "/ut/game/fifa17/tradePile/counts", Some(MarketQuery)),
|
||||
("GET", "/ut/game/fifa17/tradepile/counts", Some(MarketQuery)),
|
||||
("POST", "/ut/game/fifa17/trade/900000001", Some(MarketBuy)),
|
||||
(
|
||||
"DELETE",
|
||||
"/ut/delete/game/fifa17/trade/900000001",
|
||||
Some(MarketCancel),
|
||||
),
|
||||
// ── negatives: must stay None (proxied to Python) ──
|
||||
("GET", "/ut/game/fifa17/store", None),
|
||||
("GET", "/ut/game/fifa17/store/", None),
|
||||
("PUT", "/ut/game/fifa17/store/transactions", None),
|
||||
("PUT", "/ut/game/fifa17/store/transaction/0/extra", None),
|
||||
("POST", "/ut/game/fifa17/purchasedfoo", None),
|
||||
("POST", "/ut/game/fifa17/purchased/items/extra", None),
|
||||
("GET", "/ut/game/fifa17/hub", None),
|
||||
("GET", "/ut/game/fifa17/marketdata", None),
|
||||
("POST", "/ut/auth", None),
|
||||
("GET", "/ut/game/fifa17/watchList", None),
|
||||
];
|
||||
for (m, p, want) in matrix {
|
||||
assert_eq!(
|
||||
classify_economy(m, p),
|
||||
*want,
|
||||
"RETAIL_ROUTE_MATRIX: {m} {p}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user