fix(market): /trade/status must resolve the unlisted ids /tradePile advertises

Explains and fixes the Phase C partial failure WITHOUT changing a single wire field.

The operator saw a difference between the one-item probe (Time Remaining "-") and the
generalized rows (Time Remaining "Expired"). Cause: route coverage, not encoding.
/tradePile advertised the unlisted tradeIds while ISVIEWTRADE (GET .../trade/status)
resolved ids from the market store only -- and an unlisted pile member has no listing
row, so the poll returned an empty auctionInfo. Observed live as
`route=market-status requested=1 returned=0` repeating for the row the operator had
selected, while that same id was present in /tradePile. The client polls status for
the row it displays and degrades it when the answer is empty, which is also why no
actions were offered. The probe showed "-" only because the client had not yet polled
that id (logs of the time show only tradeIds=1000000097).

So expires, tradeState, itemData.itemState and pile were all innocent. Nothing was
guessed and no field changed: both routes now share one pile enumeration
(Server::resolve_trade_pile), so an id advertised by /tradePile always resolves on
/trade/status. The corpus predicted exactly this -- tradeId must resolve across
/transfermarket, /tradePile, /watchList AND /trade/status; we had stability but not
coverage. Same defect class as the original empty-trade/status bug.

Status still answers only the ids actually asked about, and a real auction always wins
over an inactive row for the same tradeId. Regression test covers all four cases.

Records the downgraded conclusion: "inactive" is a CONFIRMED section/lifecycle
discriminator; whether the full actionable contract is now complete is the operator's
next test. itemState/pile recovery was queued on the assumption the encoding was
incomplete -- neither was touched, and both remain the next candidates if actions are
still absent.

342 tests pass, 0 failed, clippy clean. Verified live: the six inactive ids went from
returned=0 to returned=6.
This commit is contained in:
funman300
2026-08-17 21:01:07 +00:00
parent afadb13de4
commit 11c028e6eb
3 changed files with 194 additions and 36 deletions
+57
View File
@@ -531,3 +531,60 @@ So `count` currently tracks AUCTION entries, not total Transfer List membership.
not change this until the full state set (empty / unlisted / active / expired / sold /
mixed) has been observed — it remains an open question whether FIFA 17 expects
`count` to include non-active pile members.
---
## Q2 status downgrade + the "Expired" differential EXPLAINED (2026-08-17)
Phase C was a PARTIAL pass. Corrected conclusions:
```text
tradeState "inactive" = CONFIRMED section/lifecycle discriminator
(rows persist across a fresh session and land under
TRANSFER LIST, not LISTED ITEMS)
complete ACTIONABLE unlisted representation = was UNKNOWN; see below
```
### The visual differential, and its cause
| | one-item probe | generalized rows |
|---|---|---|
| Start Price | 0 | 0 |
| Buy Now / Current Bid | `-` | `-` |
| Time Remaining | `-` | **`Expired`** |
Cause: **route coverage, not a field.** `/tradePile` advertised the unlisted
tradeIds, but `GET …/trade/status` (ISVIEWTRADE) answered them from the market store
ONLY — and an unlisted pile member has no listing row, so the poll returned an empty
`auctionInfo`. Observed live as `route=market-status requested=1 returned=0` repeating
for the row the operator had selected (`tradeIds=1000000122`), while the same id was
present in `/tradePile`.
The client polls `/trade/status` for the row it is displaying, and an empty answer
degrades it: Time Remaining renders `Expired` and no actions are offered. The earlier
probe showed `-` simply because the client had not yet polled that id — the logs at
the time show only `tradeIds=1000000097`. So `expires`, `tradeState`,
`itemData.itemState` and `pile` were all innocent, and NO field was changed.
This is the same defect class as the original `trade/status` bug: a route the client
polls being answered with an empty set. The corpus predicted it — `tradeId` must
resolve across `/transfermarket`, `/tradePile`, `/watchList` AND `/trade/status`. We
had stability but not coverage.
Fix: both routes now share ONE pile enumeration (`Server::resolve_trade_pile`), so an
id advertised by `/tradePile` always resolves on `/trade/status`. Verified live: the
six inactive ids went from `returned=0` to `returned=6`. `/trade/status` still answers
only the ids actually asked about, and a real auction always wins over an inactive row
for the same id.
Regression test: `trade_status_resolves_the_unlisted_ids_tradepile_advertises`.
### Investigations NOT needed as a result
`itemData.itemState` and numeric `itemData.pile` were queued for PE recovery on the
assumption the encoding was incomplete. The cause was route coverage, so neither was
touched and neither is implicated. If the actions still do not appear, those remain
the next candidates — and `itemState`'s vocabulary should be dumped statically with
`fifa17-recon/tools/vocab_dump.py` (already validated against the tradeState table)
rather than guessed.