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
|
/// Classify a FIFA17 economy route from method + path, mirroring the Python
|
||||||
/// oracle's route table (`utas_server.py` §1418-1553). Returns `None` for any
|
/// oracle's route table (`utas_server.py` §1418-1553). Returns `None` for any
|
||||||
/// non-economy path. Path is already query-stripped by the caller.
|
/// 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("user/credits") if get => Some(EconomyRoute::Credits),
|
||||||
Some(t) if get && t.starts_with("store/purchasegroup") => Some(EconomyRoute::PurchaseGroup),
|
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(t) if put && is_store_transaction_tail(t) => Some(EconomyRoute::StoreBuy),
|
||||||
Some("purchased") if post => Some(EconomyRoute::PackOpen),
|
Some(t) if post && is_purchased_tail(t) => Some(EconomyRoute::PackOpen),
|
||||||
Some("purchased") if get => Some(EconomyRoute::PackReveal),
|
Some(t) if get && is_purchased_tail(t) => Some(EconomyRoute::PackReveal),
|
||||||
Some(t) if delete && is_item_id_tail(t) => Some(EconomyRoute::QuickSellPath),
|
Some(t) if delete && is_item_id_tail(t) => Some(EconomyRoute::QuickSellPath),
|
||||||
Some("item") if put => Some(EconomyRoute::MoveItems),
|
Some("item") if put => Some(EconomyRoute::MoveItems),
|
||||||
Some(t) if (t == "auctionhouse" || t == "transfermarket") => Some(EconomyRoute::MarketList),
|
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),
|
Some(t) if t.starts_with("trade") => Some(EconomyRoute::MarketBuy),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
@@ -3247,4 +3269,93 @@ mod tests {
|
|||||||
assert_eq!(classify_economy("GET", "/ut/v2/game/fifa17/store"), None);
|
assert_eq!(classify_economy("GET", "/ut/v2/game/fifa17/store"), None);
|
||||||
assert_eq!(classify_economy("POST", "/ut/auth"), 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