test(host): pin the ?type= vocabulary to the client's own 30 arms

Decoded the vocabulary from the binary rather than trusting a case count:
FUN_18012ec50 is `cmp ecx,0x1d` plus a 30-entry jump table at 0x18012ed9c, each
case `mov ecx,<atom>; jmp <atom->string>`. Resolving those atoms against
fut_atoms.tsv yields the exact token list, and it matches club_type_filter
one-for-one — 30 implemented, none missing, none invented.

That is worth a test rather than a note. A MISSING arm answers a real tab with
unsupported_type and an empty screen; an INVENTED arm is worse, because it is
dead code that looks like coverage. Mutation-checked: renaming the leaguelogos
arm fails the test.

Two facts fall out that were previously guesswork. There is no
`playergoalkeeper` token — the client has only DEF/MID/FWD tabs — so a
goalkeeper appearing under playerdefender is CORRECT and not a filter bug, which
I had flagged as suspicious while sweeping. And `healing`/`contract`/`training`
exist as ?type= arms even though consumables have their own route.

Also completes the last unapplied item of plan section 7: the full vocabulary is
now written into ENDPOINT_MAP.md with how it was derived.
This commit is contained in:
funman300
2026-08-21 22:02:53 +00:00
parent d76c184cf1
commit 274838cc2e
2 changed files with 90 additions and 1 deletions
+26 -1
View File
@@ -979,7 +979,32 @@ bronze/silver/gold/any; `sort` = asc/desc; `rare` = the literal string `SP`, not
a boolean; `state` = the itemState names plus `any` — and note the REQUEST spells
it `onSale` where the RESPONSE value is `forSale`.
`?type=` has 30 values (`FUN_18012ec50`: 29 cases plus a default of `any`).
`?type=` has 30 values. Decoded 2026-08-21 from the jump table itself rather
than from a case count: `FUN_18012ec50` is `cmp ecx,0x1d` + a 30-entry table at
`0x18012ed9c`, and each case is `mov ecx,<atom>; jmp 0x180180cd0` (atom → string).
Resolving those atoms against `fut_atoms.tsv` gives the vocabulary in table order:
```
0 any 1 player 2 manager 3 headcoach
4 fitnesscoach 5 physio 6 development 7 custom
8 unlocks 9 gkcoach 10 staff 11 badge
12 kit 13 stadium 14 ball 15 equippables
16 leaguelogos 17 offlinetrophy 18 onlinetrophy 19 featuredofflinetrophy
20 featuredonlinetrophy 21 allofflinetrophy
22 allonlinetrophy 23 healing 24 contract
25 training 26 misc 27 playerdefender
28 playermidfielder 29 playerforward
```
Notes worth having: there is **no `playergoalkeeper`** — the client has only
DEF/MID/FWD tabs, so goalkeepers belong to `playerdefender`, and a GK appearing
there is correct rather than a filter bug. `healing`, `contract` and `training`
exist here as `?type=` arms even though consumables have their own
`club/consumables/<cat>` route. Six of the thirty are trophy arms.
`openfut-utas-host`'s `club_type_filter` implements all 30 with no extras; a unit
test pins the list so a missing arm (an empty real tab) or an invented one (dead
code that looks like coverage) fails the build.
**`/club/stats` has exactly seven forms**: `club`, `year`, `country/<id>`,
`league/<id>`, `newcards`, `consumables`, and the separately-dispatched `staff`.
+64
View File
@@ -6580,4 +6580,68 @@ mod tests {
Some(1)
);
}
/// The `?type=` vocabulary must match the CLIENT's exactly — no arm missing,
/// and no arm invented.
///
/// The client resolves the token in `FUN_18012ec50`, a 30-case jump table
/// (`cmp ecx,0x1d`) where each case is `mov ecx,<atom>; jmp <atom->string>`.
/// Decoding that table against `docs/fut_atoms.tsv` on 2026-08-21 produced
/// exactly the list below, and it matched this function one-for-one.
///
/// A MISSING arm answers a real tab with `unsupported_type` and an empty
/// screen. An INVENTED arm is worse: it is a token the client cannot send,
/// so it is dead code that looks like coverage.
#[test]
fn club_type_vocabulary_matches_the_clients_thirty_arms() {
// FUN_18012ec50 cases 0..=29, in table order.
const CLIENT_TOKENS: [&str; 30] = [
"any",
"player",
"manager",
"headcoach",
"fitnesscoach",
"physio",
"development",
"custom",
"unlocks",
"gkcoach",
"staff",
"badge",
"kit",
"stadium",
"ball",
"equippables",
"leaguelogos",
"offlinetrophy",
"onlinetrophy",
"featuredofflinetrophy",
"featuredonlinetrophy",
"allofflinetrophy",
"allonlinetrophy",
"healing",
"contract",
"training",
"misc",
"playerdefender",
"playermidfielder",
"playerforward",
];
for token in CLIENT_TOKENS {
assert!(
club_type_filter(Some(token)).is_some(),
"the client can send type={token} and this host has no arm for it"
);
}
// A token outside the taxonomy stays unsupported — the honest, loud answer.
for bogus in ["playergoalkeeper", "trophies", "consumable", ""] {
assert!(
club_type_filter(Some(bogus)).is_none(),
"type={bogus} is not one of the client's 30 arms and must not be \
silently mapped onto a real set"
);
}
// Absent = the main club screen, which is live-proven to be the players.
assert!(club_type_filter(None).is_some());
}
}