fix(fifa17): a dangling manager ref must not refuse the squad save

Every real squad save was failing with 400 unresolved_wire_ids. Reproduced on
staging with the repo's own seeder, which sends the captured retail body:

  route=squad-replace status=400 outcome=unresolved_wire_ids detail=[[100000427]]

FIFA 17 always sends a manager ref, and on a real profile it does not resolve to
an owned instance. scripts/sold-staging-seed-squad.py already recorded why:
production's own squad points at instance 100000427, which is absent from
production's /club/staff (1975 items spanning 100000001..100004826), and the
client accepts that squad back unchanged -- so the client never validates the
manager against the club, and the pre-0023 server accepted it.

Making the manager ownership-backed (d37a9d5 / 25f4ad1) turned that ref into a
hard refusal, which took out the primary FUT write path: no squad save means no
squad, which means the client will not enter the FUT hub at all.

A manager ref is not a squad slot. An unresolvable PLAYER slot must still refuse
the save -- committing it would silently drop an owned card from the club. An
unresolvable MANAGER ref just means there is no ownership-backed manager, which
is exactly the state before migration 0023: the save commits, the assignment is
cleared as a full replacement should, and the id is reported on
ProposedSquad::unresolved_manager_wire_id so the host can log what it could not
map instead of letting it vanish. A ref that DOES resolve is still assigned and
still authorized against the club.

Verified end to end on staging: the seeder now answers {"id": 0} with 11
occupied slots, the host logs
`manager_ref_unresolved=100000427 (saved with no manager assignment)`, and the
projected squad carries `manager: []`.
This commit is contained in:
funman300
2026-08-21 04:28:43 +00:00
parent 054a912357
commit d6aa704b01
5 changed files with 79 additions and 21 deletions
+11
View File
@@ -2131,6 +2131,17 @@ pub fn handle_put_squad(body: &[u8], deps: &SquadDeps<'_>) -> (WireResponse, Squ
);
}
}
// FIFA always sends a manager ref, and on a real profile it does not resolve
// to an owned instance (production's own save points at 100000427, absent
// from its /club/staff). That is not an error: the save commits with NO
// ownership-backed manager. Logged so a ref we cannot map stays visible
// instead of vanishing.
if let Some(wire) = build.canonical.unresolved_manager_wire_id {
eprintln!(
"utas-host owner=RUST route=squad-replace manager_ref_unresolved={wire} \
(saved with no manager assignment)"
);
}
// Commit canonical + extension atomically. No Python fallback on failure.
let req = CoreReplaceRequest {
name: build.canonical.name.clone(),
+26 -6
View File
@@ -1216,11 +1216,13 @@ fn put_assigns_the_owned_manager_and_a_later_save_clears_it() {
);
}
/// A manager ref the resolver cannot map is an unresolved wire id: the WHOLE
/// save is refused and nothing is committed. You cannot manage with a card you
/// do not own, and a save must never silently drop the assignment instead.
/// The REAL client always sends a manager ref, and on a real profile it does not
/// resolve: production's own save points at 100000427, which is absent from
/// production's `/club/staff`, and the client accepts that squad back unchanged.
/// Refusing the save would therefore break EVERY squad save, so an unresolvable
/// manager ref commits the squad with NO manager assignment.
#[test]
fn put_refuses_a_manager_ref_that_is_not_an_owned_instance() {
fn put_saves_the_squad_when_the_manager_ref_does_not_resolve() {
let items = vec![gk(), st()];
let (resolver, w) = resolver_with_wires(&items, ASSETS);
let core = FakeCore::new(items.clone(), 2);
@@ -1239,10 +1241,28 @@ fn put_refuses_a_manager_ref_that_is_not_an_owned_instance() {
Some(100_000_427),
);
let (resp, log) = handle_put_squad(&body, &deps);
assert_eq!(resp.status, 200, "{log:?}");
assert_eq!(resp.body, br#"{"id":0}"#);
assert_eq!(core.replace_calls(), 1, "the squad itself is committed");
assert_eq!(
core.manager(),
None,
"no ownership-backed manager is invented from a ref we cannot map"
);
// An occupied PLAYER slot that does not resolve is the opposite case: it
// would silently lose an owned card, so it still refuses the whole save.
let bad_player = put_body(
"f442",
w["oc-a"],
&[(0, w["oc-a"], 1), (1, 999_999_999, 9)],
"[]",
None,
);
let (resp, log) = handle_put_squad(&bad_player, &deps);
assert_eq!(resp.status, 400);
assert_eq!(log.outcome, "unresolved_wire_ids");
assert_eq!(core.replace_calls(), 0, "nothing committed");
assert_eq!(core.manager(), None, "no manager assigned");
assert_eq!(core.replace_calls(), 1, "nothing further committed");
}
#[test]