TOGBankClassic-v1.9.1
What's new
TOGBankClassic Changelog
[v1.9.1] (2026-10-17) - Safer Order Sync
Bug Fixes
- MUT-ID-001: a received order change could overwrite a different order. Peer Review eee82c0f,
Q1.
Guild:ApplyRequestMutation(Modules/RequestLog.lua) keys the merge byentry.requestIdand looks uprequests[requestId]for its permission checks, butmergeRequeststores the snapshot as it arrived, so a mutation whoserequest.iddiffered from itsrequestIdput one order's record under another's key. An "add" passes when the snapshot's requester is the sender, so a peer could replace any order with a newer snapshot of its own. Every sender in this addon sets both ids from one value, so only a mismatched or hostile peer reaches it. A mutation whose two ids differ is now refused.Tests/requestlogcoverage_spec.lua, confirmed red with the check switched off. Not wire-visible: nothing this addon sends changes. - ADD-OWNER-001: an "add" could take over or close someone else's existing order. Peer Review
e5b7e0c8, R1, the other half of MUT-ID-001. The add check only compared the snapshot's requester
with the sender, never the order already held under that id, so a peer naming itself requester
could send a newer snapshot of another player's order and
mergeRequestreplaced it: the order changed hands, or arrivedcancelledorcompletewithout the cancel and complete permission checks, which run only for those mutation types. An add for an order already held is now refused unless the sender is that order's requester; the requester's own add arriving twice still merges. The only sender of an add,Guild:AddRequest, mints a fresh id, so a modified client is needed to reach it.Tests/requestlogcoverage_spec.lua: a takeover and a cancelling add are refused (red with the check switched off), and the requester's repeated add still merges. - ADD-OPEN-001: a requester could close or fill their own order by re-sending it as an "add".
Peer Review 70aaa39f, S1, the last piece of the same class. With ADD-OWNER-001 the requester is the
one sender an add for a held order still accepts, and the add branch never looked at the
snapshot's status or Sent count, so an add carrying
complete,fulfilled, or a Sent count closed the order past the banker-only complete check. An add is a new order:Guild:AddRequestsends itopenwith nothing Sent, and the payload is serialized when it is sent, so a later change to the order never rides on it. An add with any other status, or a Sent count above zero, is now refused.Tests/requestlogcoverage_spec.lua, red with the check switched off. - REQ-GATE-002 / BYID-ASKED-001 / SYNC-OWNER-001: the full-record request sync bypassed all three
fixes above. Peer Review 385b8033, T1. MUT-ID-001, ADD-OWNER-001 and ADD-OPEN-001 live in
Guild:ApplyRequestMutation; the by-id record sync (togbank-rd2, and the olderrequests-by-idontogbank-rm/togbank-rd) reachedGuild:ApplyRequestSnapshotwith no sender, no roster check and no check that the id was ever asked for, andmergeRequestreplaces a held record wholesale when the incoming one is newer. So one whispered record from anyone, guildmate or not, could change an order's requester, status, quantity or Sent count, and one tombstone could delete it. Three checks now stand on that path:- REQ-GATE-002 (
Modules/Chat.lua,IsRequestSyncPeer): every by-id answer and every request index (togbank-ri, andrequests-indexontogbank-rm/togbank-rd, whose tombstones also delete orders) is dropped unless the sender passesIsInCurrentGuildRoster-- the same peers REQ-GATE-001 already required for the queries. - BYID-ASKED-001 (
Modules/RequestLog.lua):Guild:QueryRequestsByIdrecords each id it asks for and of whom (requestedById);Guild:TakeRequestedByIdaccepts an answer only for an asked id, from that player (any guildmate for a query sent to the guild), withinREQUESTS_SYNC.REQUESTS_BY_ID_ANSWER_WINDOW(300 s,Modules/Constants.lua, chosen, not measured), and once. An unasked answer no longer ends an index sync waiting on by-id records. An id asked of two players (an index sync and a SYNC-013 fulfill fetch can both ask for one) keeps both askees, each with its own ask time, and takes whichever answers first; a re-ask of one player does not extend another's window. Asks whose windows have all closed are swept on each new ask, so unanswered ones do not pile up for the session. - BYID-WINDOW-001 (Peer Review 35aa4d69, P1): the window counts from the later of the ask and that player's last asked answer. A big sync's answers queue behind the responder's ChatThrottleLib and can land well after the ask; a responder still sending answers is slow, not gone, so its queued answers are no longer dropped. Not measured how late they run in a client.
- BYID-EMPTY-001 (Peer Review 35aa4d69, P2): an empty legacy
requests-by-idtable answers nothing, so it no longer ends an index sync. No shipped client sends a table by-id; a sync left waiting is still freed byINDEX_INFLIGHT_TIMEOUT. - BYID-MALFORMED-001 (Peer Review 3a1998fa, P5): a
requests-by-idpayload that is not a table, or whoserequestsis not a table, answers nothing either, so it no longer ends an index sync (Guild:ReceiveRequestsById,Modules/RequestLog.lua). Each branch went red on its own first. - BYID-SYNC-END-001 (Peer Review eeb82a62, F2 and F4): the FIRST asked by-id record ended the
whole index sync while the rest were still being asked. A sync the player started printed
"Requests synced." after one record, and with
inFlightcleared only the 60 s query cooldown stood between the drain (5 s per 50 ids) and a second index sync; one to a different peer emptied the first peer's unsent batches. The sync now records the ids it asked for (requestsIndexSync.pendingById, filled byGuild:EnqueueByIdBatches) andGuild:EndRequestsIndexSyncIfAnsweredends it only once no batch is left to send and every one of them has been answered, from either receive path. A guild-wide query's 10 s "nobody answered" clear no longer ends a sync an index answer set fetching. With the sync now in flight for the whole drain,INDEX_INFLIGHT_TIMEOUT(180 s) would have cut off any drain past 36 batches, so each batch sent and each asked answer restarts the in-flight clock (Guild:NoteRequestsIndexAnswer); the timeout now measures silence, and an id never answered still leaves the sync to it.Tests/requestlogcoverage_spec.lua: four examples, all red against the old code. - BYID-SYNC-END-001, multi-chunk (Peer Review dc0455df, K1-K3): an index arrives in chunks of
20 ids, one a second, and three paths still ended a sync early. A chunk with nothing missing
called
EndRequestsIndexSyncoutright while an earlier chunk's ids were still awaited (K1). The by-id drain clearedbyIdCurrentSenderonce its queue emptied, so the same peer's next chunk looked like a new sender and wipedpendingById(K2); it is now cleared only when an index sync begins or ends. And the receiver could not tell the last chunk, so a chunk answered in full before the next arrived ended the sync between them (K3). Eachtogbank-richunk now ends withfalse, <chunks still to come>; an older client's tombstone loop stops at thefalse, so it reads the chunk as before, and a chunk from an older sender (no trailer) ends as it did. The newGuild:SettleRequestsIndexSyncends the sync only when every asked id is answered and no sender has said more is coming, checked again on each chunk and each answer -- no timer. The guild-wide 10 s "nobody answered" clear also waits while chunks are still coming.Tests/requestlogcoverage_spec.lua: K1, K2 and K3 examples, red against the code before this change in their first form, before the chunks carried the trailer (desk run 20261016-172013.129); the index-sending example now pins the trailer.docs/REQUEST_COMMS.mdno longer givesINDEX_INFLIGHT_TIMEOUTas 30 s. - BYID-SYNC-END-001, other peers and the timeout (Peer Review 5b13bb3a and e3fd248b; K4, K5, M1,
G1): every client hears every guild-wide index, so another peer's chunk during a whispered sync
switched the by-id sender and wiped
pendingById, and the sync settled on that peer's answers (K4). A sender change now stops awaiting only the ids of the batches it drops unsent. That switch also left the new peer's batches unsent while the old peer's batch timer was pending, because the stale timer clearedbyIdDrainingand bailed; only the current drain clears it now, and a sender change starts its own. Index chunks did not restart the in-flight clock, so a long index could time out mid-way (K5); each chunk now does. The in-flight timeout cleared the sync by hand but notnotifyorbyIdCurrentSender, so a later automatic sync printed "Requests synced." for the player's sync that never finished (M1); oneresetIndexSyncProgressnow serves Begin, End and the timeout, the timeout says "Request sync timed out." to a player who started the sync, and End clearsnotifyeven when muted. A chunk count that drops by more than one is a lost chunk (an AceCommQueue refusal, ACQ-004), and the sync now re-asks the peer it is reading for its whole index (G1). A guild query's 10 s check could also end a later sync, since the state table is shared; Begin now stamps agenerationthe check compares.Tests/requestlogcoverage_spec.lua: eight examples, all red with the fixes undone (desk run 20261016-174649.705). - BYID-SYNC-END-001, interleaved peers (Peer Review 79dfbfd2, P1 and P2): K4 made the hold right
but not the fetch. Two peers' indexes arriving in one sync still emptied each other's unsent by-id
batches at every sender change, and those ids were never asked in that sync.
EnqueueByIdBatchesno longer resets the queue: each batch already carries its sender, so every peer's batches share the queue andbatchTotal/batchSentare running totals per drain run;byIdCurrentSenderis gone. A by-id ask refused because that peer is offline used to abort the whole queue; it now drops only that peer's batches and the ids they would have answered, keeps asking the others, and the player's sync ends with "Request sync incomplete: a bank went offline before answering." instead of "Requests synced.". A re-ask (CRC or G1) to a peer who logged off printed "Requests synced." too;EndRequestsIndexSynctakes a failure line andQueryRequestsIndexpasses "Request sync failed: NAME is offline.".Tests/requestlogcoverage_spec.lua: the K4 "drops unsent" example is replaced by the interleaved-peers one, plus an offline-peer and a failed re-ask example; all three red with the old behaviour put back (desk run 20261016-181608.468).docs/REQUEST_COMMS.mdhas a BYID-SYNC-END-001 section. - BYID-SYNC-END-001, a peer's sent batches (Peer Review f3453dc9, N1 and N2): the offline branch
released the refused batch's ids and that peer's queued ones, but not those of a batch already sent
before it logged off, so the sync waited on them until
INDEX_INFLIGHT_TIMEOUTand printed "Request sync timed out." instead of the incomplete line (N2).pendingByIdnow maps each id to the peer it was asked of, and a refusal releases every id awaited of that peer; an id a later peer's index also listed stays awaited of that later peer.byIdQueueGenhad no writer left after P1, so the guard it fed could never fire; it is deleted, and nothing cancels the by-id drain: a sync begun mid-drain still sends the queued batches (N1).Tests/requestlogcoverage_spec.lua: an N2 example, red with the old release put back (desk run 20261016-184424.699), and an N1 example pinning the drain. - BYID-SYNC-END-001, a re-ask inside a guild-wide sync (Peer Review 5063214b, P3): a CRC or G1
re-ask of one peer went through
BeginRequestsIndexSync's full reset, which emptiedpendingById, so in a guild-wide sync the ids another peer still owed stopped holding it and "Requests synced." printed before their records arrived. A forced ask of one peer while a sync is in flight is now a re-ask: it restarts only that peer'schunksToCome, keeps every awaited id, and leaves a guild-wide sync guild-wide (inFlightstays*). A refused re-ask in a guild-wide sync releases only that peer's ids and ends as incomplete once the others answer; a refused re-ask of the sync's only peer still prints "Request sync failed: NAME is offline.".Tests/requestlogcoverage_spec.lua: two P3 examples, both red with the old reset put back (desk run 20261016-192300.784). The "10 s check" example again ends the guild query before beginning a new sync and expects that sync's peer; the re-ask that stays guild-wide past the check is its own P3 example. - BYID-SYNC-END-001, a re-ask of another peer during a targeted sync (Peer Review b0f108ef, F1):
a CRC re-ask is not gated on the sync's peer, so one corrupt chunk of another peer's guild broadcast
moved
inFlightfrom the targeted peer to that one. The targeted peer's lost chunks were then no longer re-asked (G1 matched only the new peer), and a refused re-ask ended the sync "failed" with the targeted peer's ids still owed. A re-ask of a peer other than the one the sync reads now widens the sync to guild-wide (*).Tests/requestlogcoverage_spec.lua: an F1 example, red with the old line put back (desk run 20261016-195139.056). A2 (Peer Review 20851b90): an example pins the widened case end to end -- a refused re-ask of the FIRST peer after the widening drops only that peer, prints no "Request sync failed", and the sync still waits on the widened peer's id. - BYID-SYNC-END-001, another peer's broadcast held a targeted sync open (Peer Review 20851b90,
N1;
Guild:ReceiveRequestsIndex): the "chunks still to come" count was recorded for ANY sender while a sync was in flight, while the G1 re-ask beside it was already limited to the sync's own peer. A targeted sync that overheard another peer's guild broadcast whose last chunk never arrived waited on it until the in-flight timeout, then printed "Request sync timed out." instead of "Requests synced.". The count (and the in-flight clock it restarts) is now taken only from the peer the sync reads, or from anyone in a guild-wide sync.Tests/requestlogcoverage_spec.lua: an N1 example, red with the old gate put back (desk run 20261016-205138.391). - SYNC-OWNER-001 (
Guild:ApplyRequestSnapshot): a synced record naming a different requester for an order already held is refused. No client ever changes an order's requester. - The
togbank-ricorrupt-chunk branch (Modules/Chat.lua) re-asked the sender for its index with the cooldown bypassed for anyone; it now does so only for a peer passing the same gate, so a stranger's garbage cannot buy a whisper back on every send. WHAT THIS DOES NOT DO (Peer Review 82a7a7af, T2): request sync still TRUSTS GUILDMATES. A snapshot carries no author, so the mutation path's role checks cannot be mirrored. A guildmate can still list an order in its index with a newer time and answer the fetch it provokes with a changed status or Sent count, or send an index tombstone that deletes it. These fixes stop outsiders, unasked answers and requester takeovers -- not a hostile guildmate running a modified client.Tests/requestlogcoverage_spec.lua: a non-guildmate's record, tombstone and index on every branch; a stranger's corrupt index chunk; unasked and wrong-player answers on both the record and the legacy table path; a table carrying an asked and an unasked id; a repeat and a late answer; two askees; a sweep; a slow responder; an empty table; a requester change. Each check was switched off on its own and its examples went red; a guild query answered by any guildmate stays green. KNOWN COST: a client used to merge by-id records broadcast to the guild for another member's query; it now drops them and fetches what it is missing itself on its own next index sync. Older clients still answer this client's queries unchanged, since nothing on the send side moved.
- REQ-GATE-002 (
- SUITE-SPEED-002 (in game half) (
Modules/UI/Requests.lua,labelHitFor): the invisible hover frames over the Requests tab's "Requester" and "Bank" filter labels were created again on every build of the body, on AceGUI's POOLED dropdowns, and never removed. The body is rebuilt on every visit to the Guild Bank window's Requests tab, so each visit stacked two more mouse-enabled frames on those dropdowns, and a pooled dropdown later used by any other window kept a "Filter by ..." tooltip over its label. Now there is one per dropdown frame, found again on the next build, and it is hidden when the dropdown is released. Peer Review 7f4d4fdb, S2: the first version gave the hit frame its ownOnRelease, whichscaleStockreplaced at any UI scale other than 1 (AceGUI keeps oneOnRelease), so the fix only held at 1x. The hide now lives in the body's one release handler,restoreWidgetOnRelease, andlabelHitForregisters it throughmarkForRestore.Tests/requestsactions_spec.lua: a 2x example, red with the old callback put back. Peer Review 584efe3b, P1: both filters also moved the pooled dropdown's label 3 px right, and AceGUI sets that anchor only in its constructor, so the next window to take the dropdown inherited the shift.restoreWidgetOnReleasenow puts the label back on AceGUI's own TOPLEFT/TOPRIGHT 0,0; the same example asserts it, red without the reset. Not checked in a client. - SUITE-SPEED-002, two more things a released window left on AceGUI's pooled Frame
(
Modules/UI.lua), both found by the frame-growth guard's registry walk (Peer Review 7f4d4fdb, S1):KeepAboveResizeSizersstored our bottom-row buttons on the Frame and its once-per-frame OnShow hook kept lifting them for whichever window took that Frame next; andAnchorStatusBarended the status bar at one of our icons, so the next acquirer's bar kept that anchor. The list is now cleared throughOnWindowRelease, andundressWindowputs the bar back on AceGUI's own anchors (15/15 and -132/15). Both find the status background through a new local,statusBackground: they usedstatustext:GetParent(), which while the left text breathes is the cached left host, so the re-anchor (andAnchorStatusBaritself, since it was written) could move the left host instead of the background, depending on which release hook ran first.Tests/windowchrome_spec.lua(POOL-CHROME-001) asserts the button list, the bar's anchors and the left host's anchors; it was red with the list clear off, with the re-anchor off, and with the oldGetParent()lookup. Not checked in a client. - COMBAT-KEY-001: pressing keys in combat with the Requests window (or the Guild Bank window's
Requests tab) open raised
ADDON_ACTION_BLOCKEDforFrame:SetPropagateKeyboardInput(). Player BugSack report, 7x,Modules/UI/Requests.lua:1184. The window's key handler, added so Escape closes an open filter dropdown before the window, calledSetPropagateKeyboardInputon every key press; the function is restricted (HasRestrictionsin the Classic EraSimpleFrameAPIDocumentation.lua) and blocked for addon code in combat. It is now called only when the value changes and never in combat (setPropagate), the consumed Escape's key release puts the frame back to propagating, and a body built in combat leaves the hook for the next build out of combat. In combat an open dropdown still closes on Escape, but the key also reaches the game. The window's release now takes the key scripts and marker off the frame (unhookChromeFramethroughUI:OnWindowRelease), because AceGUI pools Frames across every addon and the next window on that frame inherited our handler, as POOL-CHROME-001 found for the icons. The Inventory and Donations windows (Modules/UI/Inventory.lua,Modules/UI/Donations.lua) lost their keyboard hooks: each routed key presses toUI:EventHandler, which acts only on clicks and drags, so it did nothing, whileEnableKeyboard(IsProtectedFunctionin the same documentation) and the setter could be blocked the same way when the window was first built in combat.InCombatLockdownadded to.luacheckrcand.luarc.json.Tests/requestsactions_spec.luaasserts no setter call for an unchanged key, the key-up restore, no call in combat, no hook built in combat, and the release clearing the scripts. Not checked in a client. - COMBAT-KEY-002: a Requests window could be left blocking every keybinding. Peer Review
a21c6a53, P1, on the COMBAT-KEY-001 self-audit. A keyboard-enabled frame that does not propagate
stops all keybindings while it is shown (warcraft.wiki.gg,
API_Frame_SetPropagateKeyboardInput). If combat began between an Escape that closed a dropdown and that key's release, COMBAT-KEY-001's combat guard refused the restore and the player lost every keybound ability for the rest of the fight; if the window was then released in combat, AceGUI pooled the frame that way for any addon's next window, even out of combat. A restore combat refuses is now queued (pendingRestoreinModules/UI/Requests.lua) and run onPLAYER_REGEN_ENABLED; a frame released in combat also gets its keyboard turned off then, unless our hook has taken it back.Tests/requestsactions_spec.luadrives both cases through combat and its end. Not checked in a client.
Internal
- Changelog archive: the whole v1.7.0 section moved to
CHANGELOG_ARCHIVE.md, which left this file 285 characters under the 120,000 ceiling before the move. - Release docs for v1.9.1:
README.txtgets the version line (it still said 1.8.0) and a 1.9.1 Changelog Highlights block with its compatibility note. - SUITE-SPEED-002 (desk timing run 2026-10-16: from windowchrome_spec on, ~46,300 live frames
and a 145 MB heap, and requestsactions_spec over the 60 s per-file limit in the forward run while
passing alone). The forward run held three kinds of leftovers from earlier examples:
Tests/env_togbank.luaforgetFrameCachesonly HID the Requests body's cluster buttons, Settings overlay and list host, so they stayed children of the pooled window frame. They are now detached (SetParent(nil)), asreleaseWindowalready did for the content's frames.Tests/requestsactions_spec.luanever released the cancel-reason dialog. The module builds it once and keeps it, which is right in the client; here every example built a new one, and its buttons (AceGUI names them as globals) kept that example's module, window and list alive through their OnClick closures.unloadnow releasesR.CancelDialog.Tests/mailbox_spec.lua's window block never released the Mailbox window; itsafter_eachnow closes and releases it. Measured on the per-file timing run: live frames from windowchrome_spec on went from ~46,300 to ~16,700, and the heap from ~145 MB to ~65 MB; requestsactions_spec alone went from 15 s and 21,072 frames to 6 s and 4,354. Forward 2181 passed in one run of the default forward suite; reverse 569 + 852 + 760 = 2181. The wall times in the whole-run timing were taken on a busy machine and are not comparable run to run. New guard:Tests/requestsactions_spec.lua, "the third build-and-release holds no more live frames than the second", which counts live frames per parent chain and, on failure, names what still references a leaked frame (a walk from the Lua registry). It failed (20,160 against 21,013) before the cancel-dialog release and passes after it.browse_specstill adds ~4,000 live frames to the run (not chased further); the whole-run timing was not repeated after the mailbox_spec fix, so its effect there is not measured (alone, the file ends at 6,760 frames). Peer Review 7f4d4fdb, S1 -- why releasing a dialog the guard never opens fixed it: the walk took its root from any detached frame, including older examples' pins. It now takes one only from frames the third cycle made, andcycle()drops the file's ownR(which always reaches the newest window). With the dialog release switched off once (a desk run of requestsactions alone), it named the dialog example's unreleased Confirm button (a named AceGUI global): itspendingCancelUIled to a list whose row cells' handlers each reached the NEXT example's module and list, so one pinned dialog held every later window, one more per example. On the way it also found the Settings overlay's named scroll frame and the template's child globals (TOGBankClassicReasonsScroll*) keeping the last overlay;unloadclears them, and the two Frame leftovers above. Peer Review N1: two examples loaded a second window without releasing the first; both nowunload()first. requestsactions_spec alone now ends at 2,652 frames. On the final tree (with P1 andstatusBackground): forward in its four parts 556 + 585 + 560 + 483 = 2184; reverse 569 + 855 + 760 = 2184. A whole forward run on the same tree timed out two files at the 60 s limit while the machine read 100% CPU with another run in flight; no assertion failed.
[v1.9.0] (2026-10-16) - My Characters
New Features
- ACCOUNT-MODE-001: "My characters" in the Requests window's Requester filter. Discord, Terra
2026-10-15: "curious if an 'account mode' can be added so that all requests per-account are shown
in the requests window regardless of which toon you're on." Every character that logs in is noted
account-wide (
accountCharactersinTOGBankClassicOptionDB.global, written byEvents:PLAYER_LOGINand again whenever the Requests filters update). When the account has more than one, the Requester dropdown offers "(N) My characters" at the top, and it shows the requests of all of them. Picking it turns account mode on (requests.accountMode), so the list opens there on every character; picking any other requester turns it off. Bankers still open on their own Bank filter.Modules/Options.lua(NoteAccountCharacter,GetAccountCharacters,IsAccountModeEnabled,SetAccountMode),Modules/UI/Requests.lua,Modules/Events.lua; the Requester label tooltip and the "?" help describe it. Specs inTests/requestsactions_spec.luaandTests/optionslayout_spec.lua. A character only counts once it has logged in with this version. Checked in the Classic Era client by the operator, 2026-10-16.
Bug Fixes
SEARCH-007: the Requests search box now matches every column. The operator, 2026-10-15: "the search requests needs to filter off the data in any column, right now it doesn't". SEARCH-006 matched only Date, Requester, Bank and Item, so a quantity, a sent count or the status shown by the date's glyph found nothing.
#andSentare now markedsearch = trueinCOLUMNS, andColumnTextreturns what each column shows: the quantity as3x, the date followed by its status word (open / fulfilled / cancelled, matching the glyph), and the banker followed by the sister-guild name XGUILD-LABEL-001 prints beside it. Only Actions, a row of buttons, is not searched.Modules/UI/Requests.lua;Tests/searchbox_spec.lua(its old assertion that the quantity was not searched is reversed, and it now loads the real Guild and RequestLog modules for the status and guild name). Checked in the Classic Era client by the operator, 2026-10-16.LOCALE-SENDTRIM-001: in a guild with players on different client languages, an order mailed with the Fulfill buttons could go uncredited. Peer Review 4d7fc1c4. At send time
Mail:OnSendMail(Modules/Mail.lua) cuts each order on the mail to what is really attached, and it matched the attachment by name: the slot's name fromGetSendMailItemis in the banker's language, the order's is in the requester's, so a German requester's order on an English banker's mail matched nothing and was dropped. The send slots now record their item id (GetSendMailItem's second return), each order on the mail carries its request'sitemID(PrepareFulfillMailandFulfillStep), and the cut matches by id. Random-suffix variants share one base id, so every order first takes the slots matching its id AND name, and only then what is left by id alone; otherwise, with one variant detached by hand, its order took its sibling's slot and the wrong order was credited (found in this fix's self-audit, specced). An order with no stored id (from a pre-REQ-001 client) still matches by name, which is safe because such an order was found in the banker's bags by that same name.GetSendMailItemreturns the id second in every flavour's UI source (Classic Era, TBC, MoP, Forever; Peer Review f6aa732c).Tests/multifill_spec.lua, red with the name match. The same bug sat one call further on (Peer Review f6aa732c, F3/F4):Guild:FulfillRequest(Modules/RequestLog.lua) matched orders only by lowercased name, so a mail the banker built by hand, or a stack hand-added to an addon mail, credited nothing when the two clients' languages differed (a German banker mailing 20 Teufelsstoff left an English order for 20 Felcloth open).FulfillRequestnow takes an optional item id and matches on it when the order has one, serving every order that matches id AND name before any that matches id alone, for the suffix reason above.Mail:ApplyPendingSendpasses the slot's id from both of its loops, and the hand-added extras are grouped by id rather than by the banker's name for them.Mail:DebugSendMailStatenow reads the slots through the sametog_readSendSlotsas the send hook (F5). Decided with it: when one mail covers part of several of a person's orders for one item, the OLDEST is filled first (Guild.RequestOlderFirst, moved toModules/Guild.luafrom Mail.lua'stog_olderFirst, which now calls it); it used to be whichever orderpairs()reached first, so a partial mail could complete a newer order and leave the older one open. Because of that sort, a spill with no target order now skips cancelled orders (found in this round's self-audit): the old loop never skipped them either, but oldest first made a cancelled order left short the likeliest to take the credit ahead of the open order the items were for. A targeted send still credits its own order whatever its state, as before. FINISHED-001 (Peer Review 2073754c) widened that skip to every finished order: an order closed short ascomplete(a peer'scompletemutation is applied after only a permission check, and the load-time normaliser keeps it) took the spill the same way. "Is this order finished" is now one function,Guild.IsRequestFinished, used by the spill,CancelRequestandReopenRequest, and the three "a fill must not overwrite this status" checks read oneCLOSED_STATUStable; there were seven hand-written copies, and the cancelled-only skip already disagreed with the rest.Guild:CompleteRequesthad no caller outside the specs since COMPLETEQTY-001 moved the hand-off button toFulfillRequestById, so it is removed and its four spec uses (requestchain_spec,xguild_spec,xguildfleet_spec) callFulfillRequestById.Tests/localefill_spec.lua, each example red first (no id match and no sort: the manual mail, the extra and the oldest-first examples; the exact pass ignoring the name: the suffix example; the cancelled filter off: the cancelled example; the cancelled-only skip: the short-completeexample). The oldest-first example keys its two orders under idspairs()walks newer first and asserts that walk as a precondition, so a change in Lua's hash order fails it rather than letting it pass unsorted. One residual, LOW: in a MIXED-language guild, two suffix variants on one mail (for example "of the Bear" and "of the Monkey" bracers, which share a base id) can still be credited to each other's orders, because the id-and-name pass needs equal names and the id-alone pass takes them in order.GetSendMailItemLinkcarries the suffix and is the named remedy; not done. Not checked in a game client: a mixed-language guild was not available, and the operator closed it without one (2026-10-16, "mark the language stuff as done, i'm not going to test it").COLLECT-LAG-001: Fulfill Oldest could pull one stack too many from the mail. Peer Review bf89d59d. Taking an item out of the mail is not instant: the inbox drops it before the bags show it. In that moment the order being collected for reads short, and Fulfill Oldest serves another order. The count of what had already been pulled was one shared
collectState, replaced then; when the first order came back it was recomputed from the bag count, which did not yet include the stack on its way, and that stack was pulled again.FulfillStep(Modules/Mail.lua) now keeps the count per order (collectStates[req.id]), drops an order's entry once its bags cover it, and drops them all when the mailbox closes. One consequence kept on purpose: if the banker moves a pulled stack away before the order is sent, that order says it is waiting until the mailbox is closed and reopened, as the single shared count already did for the order in front.Tests/bankcollect_spec.lua, red without the fix (three stacks pulled for an order that needed two).MAIL-SLOTCAP-001: an order needing more than twelve stacks was mailed short and credited in full. Peer Review bf89d59d, finding B1.
CalculateFulfillmentPlannever looks at the mail's twelve attachment slots, andMail:FindOldestServiceableOrder(Modules/Mail.lua) accepted the plan whatever its size; ATTACH stopped at slot 12, and SEND credited the full quantity. An order for 13 Linen held as 13 single stacks went out with 12 and was marked filled for 13. Now a plan that does not fit is cut to its twelve largest whole stacks (tog_capPlanToMail), the mail is credited what those stacks hold, and the order stays open and oldest so the next click mails the rest. ATTACH also returns what it actually attached and SEND credits no more than that. The mailbox message says the mail is part of the order ("Order: 12 of 13x Linen Cloth for Alice ... (one mail holds 12 stacks; the rest goes next)"; the capped plan carriescappedFrom; self-audit 9da6eefd F2).Tests/multifill_spec.luadrives the two-mail case end to end, through the real send hookMail:OnSendMailbefore each send (F1). Covered by the operator's in-game confirmation of the mail section, 2026-10-16; the debug log captured shows no capped send (not checked per item). The twelve-slot cap is now one helper,tog_mailCap, at all nine sites that spelled it out, and the capped plan's never-shownreasontext is gone (Peer Review 024ada6d F2).SEND-SNAPSHOT-001: an addon send could credit nothing if the client empties the attachment slots inside
SendMail. Peer Review 024ada6d F1. The send hook is ahooksecurefuncpost-hook and read what went fromGetSendMailItem; when the client empties those slots is not in its UI source (MAIL_SEND_SUCCESSonly resets the name, subject and body boxes). The slots are now also recorded onMAIL_SEND_INFO_UPDATE(Mail:OnSendInfoUpdate, wired inModules/Events.lua), andMail:SendAttachmentsuses that record when the hook finds the slots empty -- but only when they went empty in the same frame as the send (GetTime()is frozen within a frame), so stacks the banker took off the mail earlier are never credited. The record is consumed by each send and dropped onMAIL_CLOSED. A MAIL debug line in the hook says how many slots it saw and whether it used the record.Tests/multifill_spec.luasends the capped mail through the real hook with the slots emptied before the hook and still full in it (both credit 12), and a hand-detached mail (credits nothing); each guard was switched off once and went red. Those examples fireMAIL_SEND_INFO_UPDATEandMAIL_CLOSEDthrough the realEvents:RegisterEventsregistration, so dropping either wiring line fails them (Peer Review 1aa094de F2). Measured in the Classic Era client by the operator, 2026-10-16, over three Fulfill sends in a row: the hook found 1, 2 and 3 slots still attached, the recorded update agreed each time, every order was credited, and no "Mail sent." reached the mail-error path. So on Classic Era the slots are still full inside the post-hook, and the record is a fallback for a case not seen in game (Peer Review c46172df). The test harness pin moved to WoWAPITestingbe92d12(inbox da34acf1): itsSendMailnow keeps the slots untilwow.completeMailSend(), the stand-in for the server's confirmation, soTests/multifill_spec.luaconfirms each batch send through aconfirmSendhelper instead of relying on the send click to empty them. The "slots still full" example now uses the harness's ownSendMail; the "already empty" one installs its own. The manualMAIL_SEND_INFO_UPDATEfires stay, because that describe registers the addon's handlers into a captured table the harness's event never reaches. The pin then moved on to9f4d90a(f6e5fde: a secondSendMailbefore the first is answered now raises, andSetPointbinds a frame name at call time). The fourTests/multifill_spec.luaexamples the harness named as at risk already pass, becauseconfirmSendanswers every send; the whole suite stayed green with no spec change.SUFFIX-KEY-001: an order with no random suffix and an "of the Bear" order of the same item did not hold each other's place in the queue. Peer Review bf89d59d. An order with no suffix counts every variant of its item in the bags, but it was keyed apart from the Bear order, so first come, first served did not apply between them: a newer Bear order could take the stack an older any-variant order was waiting on, and the other way round. Such orders still exist (stored before INV2-SUFFIX-001, from an older client, or placed by another addon through
Guild:AddRequest; the addon's own request dialog always carries the suffix).Modules/Mail.luanow measures each order against a pool of its competitors (tog_newPool/tog_poolAdd/tog_poolFor): two orders compete when they share a key, or share an item id and either has no suffix. The mailbox hold, the ride-along reservation and the bank shortfall all use it; Bear and Tiger orders still never hold each other up. At the bank the error is toward pulling a stack too many for a suffixed order, never toward leaving one short.Tests/bankcollect_spec.lua, red without the fix.Tests/multifill_spec.luanow loadsModules/Inventory/Record.lua, which it needed and had only been inheriting from an earlier spec file.NAME-POOL-001: an order with no item id and an order with one did not hold each other's place. Peer Review 1aa094de F1, the same class as SUFFIX-KEY-001. An order with no item id (stored before REQ-001, or from an old client) counts its item in the bags by name, so it takes every item of that name, but it was pooled only with other no-id orders. The pool now also sums by lowercased name (
tog_nameKey): a no-id order competes with every order of its name, and every id order with the no-id orders of its name. Where two items share a name it over-holds; where the stored names match it never under-holds (two orders placed from clients in different languages store different names and still do not hold each other, deferred with the other locale work). At the bank the over-hold is toward pulling a stack too many for an id order, never toward leaving one short. An order with no usable name pools by name with nothing: the key used to betostring(item), so every nameless order shared the key"nil"and a nameless no-id order held every nameless order behind it (Peer Review 104ff6f7 N1). How many no-id orders still exist is not known, so the path is kept rather than deleted.Tests/bankcollect_spec.lua, three of the first four new examples red without the fix (the fourth, a different name, passes either way by design), plus one for the nameless case.Tests/multifill_spec.lua's event-wiring examples now put back theTOGBankClassic_Coremethods and the addon globals they set, and assert it (Peer Review 104ff6f7 F3).FCFS-SELF-001: one requester's newer order could ride along on stock their own older order needed. Peer Review bf89d59d.
Mail:FindMoreOrdersForreserved stock only for other people's orders; an order of the same requester that could not ride now reserves its quantity too.Tests/bankcollect_spec.lua.The mailbox now says which order is holding the queue. When Fulfill Oldest has nothing it can send because the oldest order for an item cannot be covered yet, the message names that order's requester, quantity and item instead of a bare "no orders" (Peer Review bf89d59d, B2).
MAIL-ADDPARTIAL-001: adding an order to a mail that ran out of slots said "Click Send to complete". Found by COVERAGE-MAIL-001. When a banker added a second order to a mail already open for the same person,
Mail:PrepareFulfillMail(Modules/Mail.lua) replaced its message with the "N orders on this mail. Click Send to complete." line whatever was attached, so an order of 20 with only 10 slots' worth attached read as done. It now says "Attached 10 of 20 ... (partial) -- N orders on this mail. Click Send, then fulfill again." when the order is short. The credit was always right (the pending send carries what was really attached); only the message was wrong.Tests/mailcoverage_spec.lua. That example was not confirmed red on the old code.EVENTS-UIERR-001: a failed mail send left Fulfill answering "waiting for the last order" for up to five seconds. Found by COVERAGE-EVENTS-001.
TOGBankClassic_Events:UI_ERROR_MESSAGE(Modules/Events.lua) took(_, message), but the event's payload is(errorType, message)on every flavour this addon ships (Blizzard_APIDocumentationGenerated/SystemDocumentation.lua,UiErrorMessage, read in the classic_era, classic_anniversary, classic and forever trees). Somessagewas the numeric error type, no mail error ever matched, andMail.batchInFlightwas only cleared by the 5 s safety timerMailstarts with each batch send (Modules/Mail.lua, afterSendMailin the batch path). An earlier draft of this entry said "until a reload"; that was wrong. It now takes(_, _, message).Tests/eventscoverage_spec.luafires the event with both values; that example was not run against the old code, the failure there is by reading. Follow-up, EVENTS-UIERR-002 (Peer Review 511e37b8, L1): the message is the client's own localized text, and the test wasmessage:lower():find("mail"), so a mail failure on a non-English client still left the batch blocked. A file-localisMailErrorinModules/Events.luanow decides by the error's id:GetGameMessageInfo(errorType)returns the error's string name (Blizzard_APIDocumentationGenerated/GameErrorDocumentation.lua; Blizzard's ownUIErrorsFrame.luacalls it with the same index), and a name startingERR_MAIL_is a mail error, in any language. The English word is only the fallback when the function is absent or knows no name for the index (it is documentedMayReturnNothing). Peer Review 6639c74e (R1) proposed this; the first draft of the fix matched the text against everyERR_MAIL_*string in_Ginstead, which walked all of_Gon the first UI error of a session (combat included) and could never match a template string. The handler now names its second parametererrorType. Which error names exist and which indexes map to them is not verified (GlobalStrings are not in the local source trees);GetGameMessageInfois documented in the classic_era tree read here and, per Peer Review, in classic, classic_anniversary and forever.Tests/eventscoverage_spec.luastubs it: index 3 namesERR_OUT_OF_RANGEwith a text that says "mail" and leaves the batch alone, index 51 namesERR_MAIL_DATABASE_ERRORwith a German text and frees it, and an unknown index falls back to the English word. Not run against the old code.GetGameMessageInfois added to.luarc.jsonand.luacheckrc(read-only for the addon, writable for the Tests). Follow-up, EVENTS-UIERR-004 (Peer Review 030b8a0a, F2): any mail error cleared the batch flag, including one from an inbox action. The flag is set only by a batch send and cleared on its confirmation or by its 5 s safety timer, so the exposure was an inbox error landing while a send was unconfirmed: it freed the next click early.UI_ERROR_MESSAGEnow leaves the flag set for the twoERR_MAIL_*names that can only come from the inbox, by their enUS text in the classic_era GlobalStrings (Ketho/BlizzardInterfaceResources,Resources/GlobalStrings/enUS.lua, read on the web because GlobalStrings are not in the local trees):ERR_MAIL_ATTACHMENT_EXPIRED("That item has expired.") andERR_MAIL_DELETE_ITEM_ERROR("You can't delete that."). The text ofERR_MAIL_DATABASE_ERRORnames no side, so it may come from either and still frees the batch: a wrong free costs one early click, a wrong block costs the 5 s timer on every failed send. Inbox errors without theERR_MAIL_prefix (a full bag) were already ignored. Only the flag skips the two: they still write the MAIL debug line and put "Mail error: ..." in an open Requests window's status bar, so a failed take is still reported (Peer Review ca15a538, U1; the first cut skipped that whole branch). The English-text fallback cannot tell the sides apart and is unchanged. A gate onMail.pendingSendwas built and then removed (Peer Review 7562cf08, G1): whenever the flag is set,pendingSendis set too, so the gate never changed an outcome.MAIL_FAILEDwas not used: Blizzard's own Open All listens for it during inbox takes too (Blizzard_UIPanels_Game/Classic/MailFrame.lua), so it has the same breadth.Tests/eventscoverage_spec.luafires both inbox-only ids with a batch pending and the flag stays set; confirmed red with the exclusion removed. Which index each name has in a client, and whether those two are ever raised during a send, is not verified in a client.BATCH-TIMER-001: an earlier send's safety timer could free Fulfill Oldest while a later send was unconfirmed. Peer Review 030b8a0a (N1). Every batch send in
Mail:FulfillStep(Modules/Mail.lua) starts a 5 s timer that clearedbatchInFlightunconditionally. Send A confirms, send B starts two seconds later, A's timer fires and frees a third click before B confirms. Each send now stampsMail.sendGen, and its timer clears the flag only if no later send has stamped it since. How often a confirmation takes long enough for this to matter in a client is not measured.Tests/multifill_spec.luadrives two sends two seconds apart on the harness clock; confirmed red with the unguarded timer put back.SEND-RECIPIENT-001: a mail to one person could credit another person's order. Peer Review f4c6a142 (R1).
Mail:OnSendMail(Modules/Mail.lua) kept any pending send under 10 s old without checking who the mail was going to. Two ways in: a Fulfill Oldest send that failed leaves its pending send behind (nothing clears it on failure), and after a row's Fulfill the banker can retype the name box. Either way, a mail to Bob carrying the same item credited Alice's orders, and any extra stacks spilled to Alice's other orders. The pending send is now kept only when the mail goes to the same person, compared without case because the client sends to a name typed in any case; otherwise the mail is tracked as a hand-built mail to its real recipient.NormalizePlayerNamedoes not fold case, and Lua'slowerfolds only ASCII, so a name whose accented capital is typed in lower case falls to the hand-built path: the order loses its id targeting but nobody is wrongly credited.Tests/multifill_spec.lua: a row Fulfill for Alice mailed to Bob credits Alice nothing, and the same name typed in lower case still credits the prepared order. The first was confirmed red without the check. Covered by the operator's in-game confirmation of the mail section, 2026-10-16; the debug log captured shows no redirected send (not checked per item).
Internal
COVERAGE-MAIL-001:
Modules/Mail.luaandModules/RequestLog.luaat 100% line coverage. Peer Review 074cdad6 and 5bd709cf. Mail.lua went from 86.90% to 1138/1138 lines and RequestLog.lua from about 79% to 1560/1560, measured with the harness'scoverage.luaover the whole forward run (2094 passing). NewTests/mailcoverage_spec.luaandTests/requestlogcoverage_spec.lua, 85 examples between them. Three blocks that could not run were deleted rather than covered, each checked against its callers:mergeRequest's nil-id/nil-record guard (both callers,ApplyRequestSnapshotandApplyRequestMutation, refuse those first); the per-sender cooldown inCanQueryRequestsIndexand itsperSendermap (written in the same step aslastQueryAt, the only writer of either, so it could never refuse when the global cooldown had passed); and the "single large stack" branch at the end of Mail.lua's fulfillment plan (for a positive need PHASE 3 already splits from the largest stack, and a need of 0 returns at PHASE 1; only a negative need reached it, and it planned a negative split, which now fails closed). No production caller can pass a negative need: all four take it fromRequestQuantityNeeded, which never returns below 0.COVERAGE-EVENTS-001:
Modules/Events.luaat 100% line coverage. Peer Review 074cdad6. It went from 56.28% to 100%, measured with the harness'scoverage.luaover the whole forward run: 420/420 lines, with Mail.lua at 1144/1144 after BATCH-TIMER-001 and SEND-RECIPIENT-001 (2137 passing; reverse parts 569 + 809 + 759 = 2137). After EVENTS-UIERR-004 the gate is 2138 forward and 569 + 809 + 760 = 2138 reverse, with Events.lua at 423/423. NewTests/eventscoverage_spec.lua. It found EVENTS-UIERR-001 above.Changelog archive: the whole v1.6.1 section moved to
CHANGELOG_ARCHIVE.md(Peer Review e1e13df8). This file was 115,152 characters, 4,848 under the 120,000 ceiling.FORWARD-PARTS-001: the fallback forward parts find new spec files by themselves. Peer Review e1e13df8. The four forward parts (A8a, A6c, B3a, B3b) were writ suites listing their spec files by hand, and
mailcoverage_spec,requestlogcoverage_specandlocalefill_specwere in none of them, so a forward run split into parts skipped them. NewTests/run_forward.lua <part> <parts>slices the sorted spec glob the wayTests/run_reverse.luaalways has; both now share the discovery and slicing in newTests/run_parts.lua. Declared as writ suitesforward 1/4..4/4; the old hand-listed parts and their superseded predecessors (A, A2-A5b, A7a, B) were removed. Forward parts 556 + 547 + 508 + 483 = 2094 over all 102 files, reverse 568 + 806 + 720 = 2094 after the refactor.ENV-SWEEP-LIB-001: the test reset no longer silences once-loaded libraries. Peer Review bf89d59d, B3.
env.resetinTests/env_togbank.luawiped every AceComm/AceEvent handler, so a library that registers once per run (VersionCheck-1.0 registers its comm prefixes on AceComm itself behind a flag that never resets) went deaf for the rest of the run after the first reset. Handlers owned by a LibStub library are now kept; an event left with none is removed. The whole forward run passed with the sparing switched off, soTests/envreset_spec.luanow pins it (red without the fix, checked). Forward-run time on a machine shared with other test runs: 229 s with the sparing off, 174 s with it on, heap about 135 MB at the end in both -- one run each, so not a measured speed-up.Peer Review bf89d59d B2 (the bank search should hold newer orders behind an older one of the same item) needed no code change: orders with one item key match the same bank stock, so the bank never helps a newer order it could not help the older one with. A spec pins the review's worked case. (Orders of one item with and without a suffix do not share a key; SUFFIX-KEY-001 above covers them.)
SLOW-REQACTIONS-001: why
Tests/requestsactions_spec.luatimed out under load. Peer Review 074cdad6 A7. One forward run (20261015-141135) aborted the file at the runner's 60 s per-FILE limit; the example it named is only where the file's total crossed 60 s. The runner's per-example telemetry for that run shows the time was the reset, not the tests:reset_mswas over 90% of most examples (47-78% in a few) and most of it was garbage collection (the slowest example: 11.7 s, 11.5 s of it reset, 8.2 s collecting). The spec'sload()calledenv.reset()and thenframes.reset().frames.reset()runs one full collection (its ownwow.reset()defers its collection to it,Tests/wowapi/env/frames.lua:7288-7315); the barewow.reset()insideenv.reset()runs another, but only while the hollow frame registry is not empty (Tests/wowapi/env/wow.lua:2044). The telemetry says it was not empty:gc_n(collections per example) was 2 for eachload()in the timed-out run (4 and 6 for examples that load two or three times), and is 1 for eachload()after the change (2 and 3), in a later forward run (telemetry run 20261015T215820). The hollow frame count itself was not read. So every reset collected twice over a heap of about 52 MB (heap_kb53518 at the file's first example in the timed-out run's telemetry; one sample read). The machine was shared with other test runs (that whole forward run took 10m56s against 4m52s for a green one later the same day), and the runner's deadline isos.clock(), which on Windows is wall time (the harness's own note,run.lua:970), so CPU starvation counts against it. This halves the work; it does not make the file immune to a badly starved machine.load()now callsenv.reset({ frames = true }), the single-reset formTests/env_togbank.luadocuments: the file alone went from 10.12 s to 4.21 s CPU, 47 passing before and after, same heap and frame counts.Tests/browsepersist_spec.luahad the same pair and got the same change, and so did 12 more spec files that reset twice per example (browse,craftlistwindow,escape,hideitems,item,mailbox,mailwindow,optionslayout,search,searchbox,statusbaryield,windowchrome); each passed alone with the same count before and after. Each of the 12 diffs was read (Peer Review aa437aa8 B6). Every hunk swaps the pair for the single reset, with three exceptions. First, incraftlistwindowandstatusbaryieldthe frames reset left a sharedloadStack()/load(), and every caller now resets first (checked for both). Second,mailbox's "is ON by default" example used to runframes.reset()partway through, afterload(true)and its first assert. It now resets once at the start, so nothing resets betweenload(true)and the rest of the example. What the old mid-example reset cleared was not read. It passes either way. Third, a straywow =package.loadedspacing slip inwindowchromeis fixed. The other changes inbrowse(FINISHED-001) andoptionslayout(ACCOUNT-MODE-001) are their own entries.FINISHED-001, follow-up (Peer Review f40a8e77 H2). The Requests window's own "is this order finished" check (
Modules/UI/Requests.lua, which decides whether Cancel and Complete show) andExpireStaleRequests,PruneRequestsand the/reqscandiagnosticGuild:ReqScan(Peer Review 7b5e912d) (Modules/RequestLog.lua) now callGuild.IsRequestFinishedinstead of their own copies, so the window can no longer offer a Cancel thatCancelRequestrefuses, and/reqscancounts as done exactly the set the prune drops. No behaviour change: every path that stores an order gives it a known status (the normaliser maps anything else to "open"), so the old "status is open" test and "not finished" select the same orders.ExpireStaleRequestshad no spec at all;Tests/localefill_spec.luanow runs it on stale open, part-filled, filled, closed-short, cancelled and recent orders and checks which ones it tombstones, and one example pins/reqscan's done count (it had no spec either).Tests/browse_spec.luaandTests/requestsactions_spec.lualift the real function onto their stub Guild tables rather than copying it.REQSCAN-AGE-001:
/reqscanreported a recent finished order as expired (Peer Review aa437aa8 B2). It aged a finished order byupdatedAtalone, so one with noupdatedAtaged from 0 and counted as "done+expired" whilePruneRequests, which ages byupdatedAtand thendate, kept it.Guild:ReqScan(Modules/RequestLog.lua) now ages by the prune's own spelling, for the expired count and the age buckets, and its two labels say "updatedAt, else date"; the missing-updatedAtcount still reads the raw field. Narrow reach:AddRequeststampsupdatedAtand the normaliser fills it fromdate, so only a record that skipped both could show it. A red-first example inTests/localefill_spec.lua(a finished order with noupdatedAtand today'sdate) failed with "1 done+expired" before the change. Follow-up, REQ-AGE-001 (Peer Review ca7dc86f): the expressiontonumber(req.updatedAt or req.date or 0) or 0was written out in five places and had a flaw of its own. A non-numericupdatedAtstring won theor,tonumberturned it into nil, and the order aged from 0 without falling back todate. SoPruneRequestsdropped such a finished order at once, whatever its date. All five sites now call one file-localrequestAgeTshelper that converts each field on its own:PruneRequests,ExpireStaleRequests,ReqScan, and the duplicate pick inNormalizeRequestList. Left as it was:sanitizeRequest'svalidateTimestamp(req.updatedAt or req.date or now, now), which falls back to now rather than 0 and is the normaliser's own job. Reach is narrow, becausesanitizeRequestreplaces a non-numericupdatedAton every stored path. Only a record not yet normalised can carry one. A red-firstTests/localefill_spec.luaexample (a finished order withupdatedAt = "garbage"and today'sdatemust survive the prune and show 0 expired in/reqscan) failed with "1 done+expired" before the helper.
[v1.8.1] (2026-10-09) - Oldest Order First
Bug Fixes
- BANKCOLLECT-SHARE-001: at the bank, Fulfill Oldest skipped a newer order for the same item.
crimsonmane, Discord 2026-10-08: Fulfill Oldest "does not fulfill the oldest order first" when a
second player orders the same item plus more.
Mail:FindOldestBankFillableOrder(Modules/Mail.lua) measured every order against the whole bag count, so the stack I pulled for the older order also "covered" a newer order of the same item, and the newer one was never collected for -- it then could not be filled at the mailbox, and only that player's other items went out. The bags are now handed to open orders oldest-first, and each order's shortfall is what is left after the older orders' share.Tests/bankcollect_spec.luapins both the search and the click-through. Not yet checked in a game client. - FCFS-001: at the mailbox, Fulfill Oldest mailed a newer order ahead of an older one for the same
item. The operator, 2026-10-09: "you are supposed to fill the oldest order first. first come
first server".
Mail:FindOldestServiceableOrder(Modules/Mail.lua) picked the oldest order the bags could fully fill and skipped any it could not, so a newer, smaller order for the same item went out first and spent the stock the older order was waiting on. I had described that skip as by design in this entry and in my Discord reply to crimsonmane; it contradicted the operator's rule. Now the first order for an item that cannot be filled yet holds its place: no newer order for that item is sent until it is. Orders for other items still go out, since they do not use the same stacks.Mail:FindMoreOrdersFor(the same person's other orders riding on one mail) now leaves the stock other people's older orders need, so riding along cannot jump the queue either. The three fulfillment searches share one "oldest" ordering and one item key. The envelope's tooltip and the Requests "?" help inModules/UI/Requests.luasaid unfillable orders "are skipped"; both now describe the first-come-first-served rule, as does the Fulfill Oldest feature line on the CurseForge page. Four examples inTests/bankcollect_spec.lua, including crimsonmane's case. Not yet checked in a game client.
Internal
- SUITE-SPEED-001: the whole forward test run fits in one run again. Every spec builds a fresh
copy of the addon, and libraries that load once per run (AceComm, AceEvent and the other Ace
mixins, AceAddon's queue of addons waiting to load, VersionCheck's listener list) kept a strong
reference to every earlier copy, so the run held tens of thousands of dead frames and each reset's
garbage collection grew slower; late files took 40 seconds or more that take under one alone.
env.resetinTests/env_togbank.luanow clears those references, and a test window's close releases its tab contents instead of only hiding them. Measured: objects left afterbrowse_spec30,800 to 5,400. The forward run passes whole (1970 examples, 2026-10-09) and matches the three reverse parts (545 + 722 + 703). Clearing the handler lists means no spec can rely on an addon copy from an earlier file still hearing messages; the reverse parts passing is the check on that. No addon code changed.
[v1.8.0] (2026-10-08) - Donations Setting
The "Enable donations" setting is back, off by default, and the Donation popup can be turned off again. Checked in a game client by the operator on 2026-10-08 (the Donation popup, the tooltip toggle and the outstanding in-game checks from earlier releases all passed).
New Features
- TOOLTIP-TOGGLE-001: a setting to turn the bank stock off in item tooltips. The operator,
2026-10-07: "someone asked to be able to turn it off" / "we need to add one to settings, and have
it on by default". There was no such setting:
Modules/TooltipBankerInfo.luadrew its "Bankers:" block on every item tooltip unconditionally. General settings gain "Show bank stock in item tooltips" (db.global.bank.itemTooltipBankers, account-wide like the Mailbox switches, AceDB defaulttrue;Options:IsItemTooltipBankersEnabledalso reads a missing key or no DB as on). The check is inTooltipBankerInfo:AppendTo, the one renderer, so it also covers TOGProfessionMaster's recipe tooltips, which call it.Tests/tooltipbankerinfo_spec.luapins the default, the off state and the shipped toggle.
Bug Fixes
MAILBOX-POPUP-001: the Donation popup could not be stopped on a bank character. GUNNEY, Discord 2026-10-07: "donation window pops up on all bankers even when unchecked and I cannot get it to stop poping up". Confirmed in the code:
Mail:Scan(Modules/Mail.lua) stood aside only while the Mailbox window auto-opened, so unticking the General setting "Open the Mailbox window at a mailbox" (a global setting, so every banker at once) brought the per-mail Donation popup back in its place on every mailbox visit -- and v1.5.2 (UX-WATERFALL-001) had removed the per-character "Enable donations" box that used to turn the popup off. The only remaining off switch was "Enable for <player>", which also stops bank scanning. That "the popup returns when unticked" rule was my design choice in UX-WATERFALL-001, not a request, and it contradicted the setting's own description ("Untick to stop that").DONATIONS-BOX-001: the "Enable donations" setting is back, unticked by default. The operator, 2026-10-07: "we should have the setting to enable/disable donations back where it was, and have it unchecked by default." The per-character box returns to the Bank panel at its old place (order 2,
db.char.bank.donations,Options:GetDonationEnabled), and it alone decides whether the Donation popup opens at a mailbox:Mail:Scanno longer consults the Mailbox window's setting or verdict, so the two settings are independent. The AceDB default changes fromtruetofalse; AceDB never wrote the old default to disk, so every character that never touched the box starts unticked. This replaces the first fix above, which had tied the popup to the Mailbox window's setting a second way. KNOWN COST: a banker who ticks both gets both windows, as before v1.5.2; gifts taken through the Mailbox window are credited either way.Tests/donations_spec.luapins the popup following the box alone, the box's place, key and OFF default, and the real accessor.PKG-EMBED-001: the CurseForge app stopped installing LibDBIcon for Classic Era, TBC and MoP. The copy of LibDBIcon bundled for WoW Forever in v1.7.1 (
Libs/LibDBIcon-1.0/LibDBIcon-1.0.lua, taken from TOGProfessionMaster) began with the packager's--@curseforge-project-slug: libdbicon-1-0@marker. The BigWigs packager greps every packaged.luafor that marker and records the slug asembeddedLibrary(release.sh:1882-1889), overriding.pkgmeta's required dependency, so the upload told CurseForge LibDBIcon was embedded and the app did not install the standalone addon -- while the Era, TBC and MoP TOCs list it under## Dependenciesand never load the bundled copy. Marker removed; the harness.pkgmetachecker now reportslibdbicon-1-0=requiredDependencyfor the upload, andTests/wiring_spec.luafails if the marker comes back. Whether v1.7.1's CurseForge file actually shows LibDBIcon as embedded was not checked on the site (not verified). The design is the operator's (2026-10-07): bundled ONLY for WoW Forever, an external required dependency everywhere else..pkgmetaalso lost all its comments, per the operator's no-comments rule of 2026-10-03; release.sh skips whole-line comments, so that change alters nothing uploaded.
Internal
- HARNESS-PIN c2fd446: Tests/wowapi moved from a49127f. Taken for WoWAPITesting's fix to
harness thread
1a91c3ca. Under load the runner's per-file TIMEOUT used to escape as a barelua.exe: (error object is not a string), with no file named; it is now a named failure. Adoption fix: twoTests/searchbox_spec.luafixtures built a FontString with no font, which the harness now refuses as the client does (FontString:SetText(): Font not set); they useGameFontHighlightSmall, the real cells' font. Production was already right (Requests.luagives every glow copy a font object). - GATE-LOAD-001: the gate survives a busy machine. run.lua's per-file limit is wall time on
Windows (WoWAPITesting, same thread), so it tripped whenever other desk runs shared the machine.
Every forward part now passes
--timeout=240, andTests/run_reverse.luapasses--timeout=600(browse_spec ran past 240 s late in the reverse order with three other runs in flight, and passed 697/0 at 600 s). Forward B2 is split into B3a and B3b, because under load its 52 files outran the desk's 15 minutes.CLAUDE.mdnames the new parts. - BOARDS-REMOVED-001: the last boards are gone.
docs/LIBRARY_CONTRACTS.md,docs/DEPENDENCY_CONTRACTS.md,Tests/HARNESS_CONTRACT.md,docs/contracts.mdanddocs/AUDIT.mdwere imported into the writ inbox and deleted. Writ, 2026-10-07: the operator's directive is "the boards should be removed after a migration"; git history keeps them. All 81 imported items were triaged the same day: 79 were already delivered, fixed or board prose. The other two were closed with a comment-level change: LOG-001's self-logging is kept on purpose and now says so (Modules/Output.lua), and Q1's false "nil when there is no link" claim was corrected (Modules/MailInventory.lua).
[v1.7.1] (2026-09-29) - WoW Forever
TOG Bank on WoW Forever, the fourth supported client. Checked in a Forever client by the operator on 2026-09-29: "everything seems to be working right now ... the display and everything is working, no more lua errors", and order highlighting "works now as intended". What that session could NOT check, for lack of other guild members running the addon: sync between players on Forever, and other players' names arriving over addon messages there.
New Features
- FOREVER-001: WoW Forever is a supported flavour (
TOGBankClassic_Camelot.toc, new, Interface 16001; the operator, 2026-09-28: "i have wow forever now, we need to onboard to make that a supporeted version with it's own .toc and updating the .ps1"). The fourth TOC is the Era TOC with one deliberate difference: LibDBIcon-1.0 is not published for Forever (it is absent from the_classic_beta_install; TOGProfessionMaster's Forever TOC embeds it for the same reason), so a hard## Dependencies:entry would stop the whole addon loading there. That TOC lists it under## OptionalDeps:instead and loads a bundledLibs/LibDBIcon-1.0/LibDBIcon-1.0.lua(MINOR 56, the copy TOGProfessionMaster ships) straight after LibDataBroker; LibStub keeps the higher MINOR if a standalone copy is also installed. TEMPORARY: once LibDBIcon-1.0 ships for Forever, make it a dependency again and delete the bundled copy. Every other dependency (Ace3, AceCommQueue-1.0, VersionCheck-1.0, DeltaSync, LibAceGUIWidgets, GuildRoster, ItemDB, ProfessionDB) already declares Interface 16001 or ships a_CamelotTOC. Not yet checked against the Forever client API (F:\Blizzard API Docs\wow-ui-source-forever) and not yet run in a Forever client. - FOREVER-HIGHLIGHT-001: order highlighting reaches the WoW Forever bank.
Modules/ItemHighlight.lualooked only for Classic'sBankFrameItemNbuttons and the-1vault, neither of which Forever has. Its bank is the tabbedBankPanel(ForeverCamelot/BankFrame.xml:85), which draws one tab from a pool of buttons.UpdateBankPanelHighlightingwalksBankPanel:EnumerateValidItems()and reads each button's ownGetBankTabID()/GetContainerSlotID(). A tab switch re-inits the pooled buttons with no bag event (GenerateItemSlotsForSelectedTab), so that is hooked to refresh, and the refresh clears every button it dimmed first. The dim-or-clear step for one slot is now one helper,HighlightBankButton, which the Classic vault and bank-bag loops also use. Spec'd inTests/itemhighlight_spec.lua. Not run in a Forever client. The icon key on Forever's bank item button (iconorIcon) was not found in the Forever source; both are tried, and a button with neither is left undimmed rather than raising. - FOREVER-HIGHLIGHT-002: the bags too. The operator, on Forever with no orders open, ticked
"Highlight needed items" and nothing in the Combined Backpack greyed. That is not a Forever
difference in what is needed: every item should grey. The bag path looked up Classic's
ContainerFrameNItemMbuttons, and Forever's modern container frames (ContainerFrameCombinedBags, andContainerFrameContainer.ContainerFramesfor split bags) draw pooled buttons with no such names.UpdateModernBagHighlightingwalks each shown frame'sEnumerateValidItems()and reads each button'sGetBagID()/GetID()(ForeverMainline/ContainerFrame.lua:560,:566-567). It answers false on a client without those frames, so Classic Era, TBC and MoP keep the Classic path unchanged. Not run in a Forever client. Whether Forever's own button refresh resets the grey before our next refresh is not known.
Bug Fixes
- FOREVER-002: TOG Bank raised a Lua error at login on WoW Forever (the operator, from game,
2026-09-28:
bad argument #2 to '?' (Usage: local success = self:HookScript(scriptTypeName, script [, bindingType]))atModules/TooltipBankerInfo.lua:117). The Forever client's GameTooltip has noOnTooltipSetItemscript -- nothing inwow-ui-source-foreverreferences it -- and builds item tooltips throughTooltipDataProcessor.AddTooltipPostCall(Enum.TooltipDataType.Item, ...)(Blizzard_SharedXMLGame/Tooltip/TooltipDataHandler.lua:199).TooltipBankerInfo:Initializenow tries the old hook underpcallfirst, so every client that has it keeps exactly its behaviour, and falls back to the item post-call, limited to GameTooltip (the only tooltip the old hook reached).pcallrather thanHasScript: the offline harness does not claim the client'sHasScriptsemantics.Tests/tooltipbankerinfo_spec.lua: a client refusing the script gets the post-call, which renders throughAppendToon GameTooltip and leaves any other tooltip alone.TooltipDataProcessoradded to.luacheckrcand.luarc.json. Not yet re-run in a Forever client. - FOREVER-NAME-001: a banker on WoW Forever was not a banker to itself, so the Bank settings page
never appeared (the operator, 2026-09-28: "i set myself as gbank, and show up on the roster ...
but i don't have the options in the settings menu to make myself visible as a banker";
/togbanklisted them asQuebeldorf Flamehammer). Forever characters have a first name and a surname: the roster listsFirst Surname, andUnitName("player")returns the two as separate values (Blizzard'sBlizzard_FrameXMLUtil/Camelot/NameUtil.lua:4-12joins them withConstants.CharacterNameSeparatorConsts.CHARACTERNAME_SURNAME_SEPARATOR).Guild:GetPlayerused only the first, so the player wasQuebeldorf-Realm, matched no roster key,IsBank(self)was false andOptions:InitGuildnever registered the page. The same wrong name reached every self-check (own scans, own requests, own-echo guards). NewGuild:GetPlayerFullName()(Modules/Guild.lua) appends the second value as a surname only on a client that has that separator constant and only when it is not the realm (the documented second return isunitServer, andUnitPopupUtils.lua:126shows the slot can carry either); it is used byGetPlayerand the request dialog's fallback (Modules/UI/Search.lua). Era, TBC and MoP have no such constant and keep the bare name.Tests/guild_spec.lua: four examples (surname joined and IsBank true; realm in the second slot ignored; a surname already in the first slot not doubled; no constant, second value ignored).Constantsadded to.luarc.json. Not yet re-run in a Forever client, and other players' names arriving over addon messages on Forever have not been checked for the same split. - PENDING-STATE-001: a banker's own bank read "Old format" between its scan and its first
publish (the operator, 2026-09-29, on a brand-new Forever banker's Bankers tab: "there is only
one format of data on newer clients, right?").
Guild:GetAltStalenessreturns"v1"for any held copy with no canon, and a canon is minted only AT publish -- so a banker whose scan was stored but held by the MULTIPC-001 publish gate (the guild had not answered yet; the status bar's countdown) was labelled old-format. New state"pending": our OWN bank, no canon, andBank.deferredset. The Bankers tab says "Not published yet" in yellow (Modules/UI/Browse.luaSTATE_TEXT / STATE_COLOR / STATE_RANK), the Inventory tab is not painted red and its tooltip explains the hold (Modules/UI/Inventory.lua). A later version named by another PC still wins ("behind"), and anyone else's canon-less copy is still "v1". Every flavour, not only Forever -- any new banker in a quiet guild saw it.Tests/tabstaleness_spec.lua: one example walking pending -> behind wins -> another banker stays v1 -> published is current. - FOREVER-BANK-001: on WoW Forever every bank-character scan raised an error, so a banker's items
never reached the Guild Bank window (the operator, 2026-09-29, with BANK debug on:
OnUpdateStop called/Calling Scan()and nothing after, thenInventory/Scan.lua:77: bad argument #1 to 'GetContainerNumFreeSlots'fromBank:Scanon closing the bank). Forever has noBANK_CONTAINER,NUM_BANKGENERIC_SLOTSorNUM_BANKBAGSLOTS-- the Forever tree defines onlyNUM_BAG_SLOTS-- because its bank is the modern bank-TAB system: each purchased character tab is a container whose id comes fromC_Bank.FetchPurchasedBankTabData(Enum.BankType.Character), which is how Forever's ownBlizzard_UIPanels_Game/Camelot/BankFrame.lua:91-98counts them. NewTOGBankClassic_Constants.BankContainers()(Modules/Constants.lua) is the one list of the character's bank containers: on Classic Era, TBC and MoP the vault then the bank-bag range, the same ids in the same order as before; on Forever the character tabs. The ACCOUNT (warband) bank is left out on purpose -- it is shared by every character on the account, so each alt would publish the same items as its own.Inventory/Scan.luawalks the tabs when there is noBANK_CONTAINER(Scan:ScanBankTabs; the Classic branch is untouched) and treats tabs reporting no slots as "bank out of sight", keeping the stored bank rather than emptying it (INV2-VAULT-001).Bank:FindItemsInBankandMail:FindEmptyBankSlotwalk the same list; on Forever both would have raised the same way.Tests/scan_spec.lua: four examples (both character tabs read and the account bank not; no tab or no slots returns nil; a whole ScanAll completes; the Classic list is the vault then BankBagRange).Tests/itemhighlight_spec.lua's BANKSLOT-001 one-spelling guard acceptsBankContainersas the shared spelling and pins that it is built onBankBagRange.C_Bankadded to.luacheckrcand.luarc.json. FOREVER-BANK-002 (the same session's self-audit): the tabs are read only while the bank window is open (Bank.atBank, set onBANKFRAME_OPENED, cleared after the close's own scan inModules/Events.lua), because a tab reporting slots is not proof its contents are readable away from the bank, and an empty read there would have replaced the stored bank on the next vendor or mailbox scan;scan_specpins nil away from the bank. Item highlighting on Forever's bank followed as FOREVER-HIGHLIGHT-001, above. - FOREVER-NAME-002: on WoW Forever a character's name carries no realm, and TOG Bank added one
(the operator, 2026-09-28: "could it have something to do with the fact that Forever doesn't
have <name><realm> it has <first name><last name>"). Forever runs regional unique names
(
RegionalUniqueNamesEnabled(), which exists only in the Forever tree): the full name is the identity, with no realm. The guild roster returns it bare, Forever's own AceDB keys the character asFirst Surname(AceDB-3.0.lua:263-278in the_classic_beta_install), and LibGuildRoster keys its roster by the bare full name (LibGuildRoster-1.0.lua:1264,:1345,:1659). TOGBank'smemberRosteris keyed by the library's names (Guild:_RefreshFromRosterLib), but every lookup went throughNormalizeName, which appended-<realm>, so on Forever every direct roster lookup (IsBank's fast path, IsInCurrentGuildRoster, IsPlayerOnline's fallback, whisper targets) missed. On a regional-names clientNormalizeNamenow keeps a bare name bare and drops a realm suffix, andGetPlayeris the bare full name -- the library's rule, in the library's words. The earlier FOREVER-NAME-001 surname join is now gated on the same switch instead of on the separator constant. Era, TBC and MoP have noRegionalUniqueNamesEnabledand take the old path unchanged.Tests/guild_spec.lua: five examples (the player is the bare full name and IsBank finds them under the library's key; a realm suffix is dropped; a surname already in the first slot is not doubled; no switch, or a switch answering false, changes nothing). The zero-data banker record saved under the old-ClassicBetaPvEkey is removed by the existing stale-stub cleanup at the next roster rebuild. Not yet re-run in a Forever client. - FOREVER-EVENTS-001: on WoW Forever the addon stopped half-way through setting up its event
handling, so a banker's bags were never stored and the Guild Bank window stayed empty (the
operator, 2026-09-28: bags full, Guild Bank window "0 items across 0 banks", no debug output with
BANK and uncategorized debug on).
Events:RegisterEventscalled the bareChatFrame_AddMessageEventFilter, which on Era and Forever alike is only a deprecation alias ofChatFrameUtil.AddMessageEventFilter(Blizzard_DeprecatedChatInfo/Deprecated_ChatFrame.lua:51, loaded only while theloadDeprecationFallbacksCVar is on). On the operator's Forever client it was nil, so the call raised and everything after it was skipped: the mailbox hooks, the share timer andeventsRegistered. It now callsChatFrameUtil.AddMessageEventFilter, the same function the alias points at on Era, TBC and MoP (ChatFrameFilters.lua:195in each tree), with the alias only as the fallback. A sweep of every global Forever defines only in itsBlizzard_Deprecated*files found two more, both in the debug chat tab (Modules/Output.lua):ChatFrame_RemoveAllMessageGroupsandChatFrame_RemoveAllChannels, now called as the chat frame's ownChatFrameMixinmethods, present on all four clients.Tests/events_spec.lua: RegisterEvents finishes with the alias absent and installs the offline-whisper filter throughChatFrameUtil. Not yet re-run in a Forever client; whether this is the whole reason the scan stored nothing there is NOT established -- the empty debug output is consistent with it, but the scan's own path was not shown to raise.
Internal
- FOREVER-001: the lockstep specs cover the Forever TOC.
Tests/wiring_spec.luapins it as the Era TOC with only its Interface line and the LibDBIcon difference changed, so a module added to one TOC only still fails; the per-TOC "is this module loaded" examples inbrowse,item,logapi,mailbox,pricelist,sendresult,togtoolsbridge,usable,windowalphaandguildroster_integrationnow include it. LIBDBICON-DEP-001's "not vendored" example keeps the three other TOCs and names Forever as the exception. wow-version-replication.ps1also mirrors into_classic_beta_, the WoW Forever install.- Harness pin
ee56a45->a49127f;frames.setScreenSizeadopted.Tests/browsepersist_spec.lua'sstandUpnow makes oneframes.setScreenSize(2560, 1440)call. It replaces three hand writes, one of them into the harness's privateUIParent._fixedRect(harness thread36d5a55f). The undragged-window scale example is back at scale 2. Its 1.25 workaround was hiding a gap in my own hand-rolled resize, not a harness defect. - REVERSE-TIMEOUT-001: the reverse runner passes
--timeout=240. This pin brings in a 60 s per-file CPU limit. In the a-end reverse slice,browse_spectook 97 s where it takes 9 s alone, because everyframes.reset()fully collects what earlier files left alive (~450 MB, ~150k frames by then). All 696 examples passed once the limit was raised.Tests/run_reverse.luanow also forwards flags after<part> <parts>torun.lua(--times), so a slice can be measured. The timeout was escaping as(error object is not a string)with no file named; reported to WoWAPITesting on thread5f29b50aand fixed there in2b63397(now a namedTIMEOUTline and one failure). The harness's new.pkgmetachecker (pkgmeta.lua) reports no FAIL here; its six WARNs are the defensive ignore entries that match no tracked file today (tmpclaude-*,*.bakand the like), kept on purpose. KNOWN COST: a file that slows down in reverse order is now caught at 240 s, not 60. The retained heap is still SUITE-HEAP-001's. - SPEC-NIL-001: 19 weak
is_nilassertions strengthened in the five spec files above that writ's new spec-shape check flagged (Writ inbox thread004cbf2f:is_nilon a literal key passes for ever if the literal is misspelt). Removed members are now pinned by an enumerated key set (browse,item); a state that is cleared is first asserted present (mailbox,pricelist,guildroster_integration). The PriceList debounce example now holds the timer, so it sees the latch set, one publish per burst, and the latch released -- it previously ran the timer synchronously and never observed the latch at all. The remaining 59 sites in 22 more spec files followed the same day, so writ's spec-shape check is clean repo-wide (Tests/wowapiexcepted, per the thread). The method, in order of preference: a whole-tableassert.samewhere the table should hold exactly something (L.opening,info.alts, the answer kinds a responder sent); an enumeration of keys by pattern where a removed member must not come back under any name (snapshot helpers, the Escape and store settings, the old-wire observer, vendored libraries); anassert.is_not_nilon the same expression BEFORE the state that clears it (batch state, pending sends, the legacy rows); a local constant shared by fixture and check for bracketed string keys, which the checker does not pair; and the public API where one answers the question (D:Entries(),Guild:IsBank,Switches:IsEnabled). - LAGW-PERSIST-SCALE-001 + LAGW-FORGET-001: TOGBank's window-scale stand-ins retired onto
LibAceGUIWidgets MINOR 35/36 (released as v0.2.7). The library's
PersistWindowfollows a live scale change itself for an AceGUI Frame and re-evaluates a FUNCTION floor on each change, soUI:PersistWindownow hands it the max(1, scale) floor rule asfloorAt(px)and registers no listener of its own;Persist_OnScaleChanged, thewindow._togPersistrecord andUI:SetPersistedAnchor(SCALE-DOCK-001's re-dock hook, and its two callers inRequests.luaandSearch.lua) are deleted. The re-dock hook existed only because the old listener re-ranPersistWindow, whoseApplyStatuscleared a hand-made dock; the library's listener raises size and bounds only, so a docked window keeps its anchor with nothing registered.UI:ForgetPersistedWindowis nowW:ForgetWindow(which also restores a ClearFrame handle's own floor, SCALE-FLOOR-002). An older library copy withoutForgetWindowkeeps the pre-35 hand-removal of the library record, and gets NO floor (LAGW-OLD-FLOOR-001, below).browsepersist_spec: the live-scale examples pass unchanged on the library's listener alone; the released-window example checks the library's record; SCALE-DOCK-001's three examples now pin a dock surviving a scale change with nothing registered, a control provingApplyStatuswould clear it, and a released docked window left alone by the next scale change. Not run in a client. - DS-422-001 / CLAMP-SCREEN-001: 11 offline failures fixed, all from two sibling-library releases
the specs had not caught up with (the addon code was right in each case). Whole suite now
1939 forward and 1939 reverse, 0 failed.
- DeltaSync v4.4.0 (MINOR 22) parks a broadcast whose numbers a listener cannot yet resolve and
replays it once the table lands. A viewer now requests straight from the banker's replayed login
broadcast, with no offer and no version query, seconds after adopting the table.
fullsync_specpinned the old offer + query round (4 examples): the first-sync example now asserts the shorter path and that it goes to the author; "four cold viewers" counts the slots while they drain; "already current" counts offers from the second broadcast on; SYNCED-001 reads "pending" as the broadcast leaves, not two seconds later when the receipt has genuinely arrived.congestion_spec's park example stamped its offerv = 99, and a table version is a timestamp, so 99 now reads as a table BEHIND the viewer's and is dropped rather than parked; it uses the viewer's own version. - LibAceGUIWidgets v0.2.7 (MINOR 36):
PersistWindowclamps a saved window to the screen.browsepersist_specsaved windows 1200-2000 wide on the harness's 1024x768 screen (6 examples). Its fixture now stands up a 2560x1440 screen (tunable, size AND the anchor rectangle, guarded byscreenMismatch), saved tops sit on-screen, and the undragged-window example uses a 1.25 scale: that window's CENTER still resolved against a 1024x768 parent after the resize (cause not established), so at 2x it hung off the left edge and the clamp correctly moved it. TOGBank's own scale listeners were searched for the cause: only the standalone Requests window re-runsPersistWindow(Requests_OnUIScaleChanged,Modules/UI/Requests.lua:1345); Browse's (Browse.lua:1181) only re-lays its lists. A harness helper that resizes the screen coherently is requested on WoWAPITesting's inbox (harness thread36d5a55f). Delivered and adopted later in this release (the harness-pin entry above): the_fixedRectwrite and the 1.25 scale are gone. - Coverage moved, stated so nobody looks for it here:
fullsync_spec's "four cold viewers" now drives four requesters through DeltaSync's replay path, so the simultaneous-login-broadcast storm is covered only bycongestion_spec's storm example.
- DeltaSync v4.4.0 (MINOR 22) parks a broadcast whose numbers a listener cannot yet resolve and
replays it once the table lands. A viewer now requests straight from the banker's replayed login
broadcast, with no offer and no version query, seconds after adopting the table.
- LAGW-OLD-FLOOR-001: an outdated LibAceGUIWidgets (no
ForgetWindow, before MINOR 35) is handed no window floor (Peer Review on self-audit587af509, finding 2). Such a copy never gives a released ClearFrame's resize handle its own floor back, and AceGUI's frame pool is shared by every addon, so TOGBank's floor would have ridden a pooled frame into another addon's window. With no floor set there is nothing to leak. KNOWN COST: on an outdated library TOGBank windows can be dragged below their minimum;UI:PersistWindowprints one warning per session asking the player to update the library.browsepersist_spec: an outdated stand-in is handed width and height only and warns once across two windows; the control shows a current library still gets the floor as a function.
Releases v1.7.0 and older have been moved to
CHANGELOG_ARCHIVE.mdto keep this file under GitHub's 125,000-character release-body limit. Nothing was deleted; each section moved whole, at a version boundary.
This mod has no additional files