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
+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());
}
}