promotional bannermobile promotional banner

Fast Guild Invite - Revived

A WoW Classic AddOn to help recruit new members into your guild.
Back to Files

FastGuildInvite-v2.13.5

File nameFastGuildInvite-FastGuildInvite-v2.13.5.zip
Uploader
PmptastyPmptasty
Uploaded
Sep 17, 2026
Downloads
582
Size
7.4 MB
Flavors
RetailMoP ClassicClassic TBCClassic
File ID
8905793
Type
R
Release
Supported game versions
  • 12.1.0
  • 12.0.7
  • 12.0.5
  • 11.2.7
  • 5.5.4
  • 4.4.2
  • 3.4.5
  • 2.5.6
  • 1.15.9

What's new

<FGI> FastGuildInvite

[v2.13.5] (2026-09-17) — Recheck is deleted, and Seen counts sightings on its own with a user-set gap

New — "Seen again after (hours)", Settings → General → Scanning, default 4

DB.global.seenAgainHours, a range slider (0.5 to 48 in half-hour steps) directly under "Keep the invite queue between sessions", since it only means anything for a queue that lasts. The operator's number: "change it from a day to an exposed user setting? set it at default to 4h". Read through fn.seenAgainAfter(), which returns seconds and applies the same default when the key is absent, because AceDB fills a default only into a fresh table and a saved-variables file from before this release has no such key. Listed in Modules/FGI_SettingsProfile.lua's catalogue under Scanning, so a template can carry it.

Changed — Seen is a count of sightings, folded inside the gap, written only by the ordinary scan

Spacedoc, via the operator: "wasn't idea to do recheck just doing normal search and if you see him next day or few hours later that he get +1 there, and that i can still do invite without recheck." The operator: "rip out recheck and do what he wants. i didn't want recheck, he did."

Seen used to be a count of DAYS: fn.recordQueueSeen folded every hit on one calendar day into one entry, so a candidate re-found a few hours later showed nothing new, and the only way to move the column inside a day was the Recheck button. It is now a count of SIGHTINGS. A sighting is recorded when a /who result re-finds a queued row and at least fn.seenAgainAfter() has passed since the row's last entry; a repeat inside the gap is folded, so the overlapping queries a subdivided sweep issues still count once, and the gap is measured from the last COUNTED sighting so a player found every hour still counts again every four. The row's seen list now holds epoch seconds (fn.now(), a new seam beside fn.today() because time is a file-local in functions.lua), one entry per sighting, capped at 30. Rows saved by earlier builds hold day strings: those carry no moment to measure from, so the first re-sighting of such a row counts, the record continues in seconds, and the column, which reads #seen, counts both kinds.

A first draft of this release counted once per pass of the level range and again on a new day, keyed by a passKey advanced at every wrap and on Reset. The operator replaced that with the gap setting the same day; fn.beginSeenPass and the key are gone, and its absence is pinned beside the recheck's.

fn.queueSeenDays is deleted: it had no production caller since the Scan tab started reading the row's own list.

Changed — the Recheck button, /fgi requeue, /fgi requeuestop and the presence probe are deleted

Three generations of Recheck went before this one: a /who per queued name (v2.11.2), a pool-derived level query whose result filter starved every other caller (v2.12.0), and the silent addon-message presence sweep (v2.12.1 to v2.13.4). With the column filling in from ordinary scanning, none of them had anything left to do. Gone:

  • Modules/FGI_Presence.lua (the probe, the sweep, presenceProgress, its CHAT_MSG_SYSTEM handler) and its line in all six TOCs and in Tests/support/addon.lua.
  • fn.refreshQueueRun, fn.refreshQueueStop, fn.refreshQueueStatus (functions.lua).
  • The requeue / requeuestop arms of the slash dispatch and the helpRequeue help line (FGI_Core.lua, Locale/enUS.lua).
  • The Recheck strip button, RECHECK_BTN_W, its label repaint, and L["Recheck"] / L["Recheck the queue"] with the button's tooltip body (GUI/Tabs/Scan.lua, Locale/enUS.lua). Row 2 of the strip ends at Reset, so ROW2_FIXED_W falls from 598 to 522: the all-hidden floor drops from 668 to 592 and the row-1/row-2 crossover moves from 5/4 counters to 3/2. The all-shown floor stays 840, since Row 1 binds there and did not change.
  • MainWindow:SetPresenceProgress and the green "Checking who's online" status-bar branch (GUI/MainWindow.lua). Modules/Scan.lua's addon.nameFromFormat export stays: the comment naming Presence as its consumer is corrected, the parser is still the one place system messages are read.

The Seen header tooltip no longer points at a button; README.md and the CurseForge page lose the Recheck paragraphs and the two command rows.

Tests/zz_presence_spec.lua is rewritten to assert that no TOC and not the offline load list names the deleted module, the one thing a stale TOC line or a half-done merge could silently break.

Tests. Tests/zz_queue_seen_spec.lua is rewritten for sightings, driven through the fn.now seam: the fold inside the gap, the count once the gap has passed measured from the last counted sighting, the next-day count, the setting honoured at one hour, the four-hour default with the key absent, a legacy day-string row continuing in seconds, the 30-entry cap, and an end-to-end example through the real /who callback (two overlapping queries then a sweep five hours later: two sightings). Its absence block now names every deleted driver plus fn.queueSeenDays, fn.beginSeenPass and FGI.Presence. The Recheck blocks in zz_scan_strip_actions_spec.lua and zz_slash_misc_spec.lua, helpRequeue in zz_slash_help_spec.lua, and the presence module's own spec are removed with the feature, each with the reason written in place. zz_scan_minwidth_spec.lua is re-baselined to the new Row 2 (624 at three counters, 592 at two and at none, floor >= 592). zz_add_new_player_spec.lua's seed examples are reworded for sightings.

[v2.13.4] (2026-09-16) — every pass starts from the configured range, rotation arrows and Assign to work, guild filter edits reach the guild, and a leaver is not welcomed

Fixed — a completed pass replayed the last pass's split pieces instead of rebuilding from the range

Field report (Spacedoc via the operator, retail screenshot): range 1-90, Scan complete, and Next scan: 1-5. Every level split removes the capped query and leaves its halves in whoQueryList, so a finished pass is a list of LEAVES, and fn:nextSearch's wrap branch fired that list again. That froze every cut at the first pass. Since v2.13.3 a cut comes from where that query's 50 rows were standing (fn.levelSplitPoint), so replaying it replays a sample the realm no longer matches, and the pile cut only ever ran once. The same freeze meant a range or custom-scan edit made mid-pass never reached a later pass until Reset.

The wrap now clears the queue and rebuilds it from the configured sources, as the Scan Groups path already did per pass. The queue construction moved out of fn:nextSearch into fn.freshPassQueue() (pure: selected custom scans with their flags, then the effective level-range sweep) and a local populatePassQueue() (anti-spam expiry, then Scan Groups or freshPassQueue), shared by the empty-queue start and the wrap. The Scan tab's Next scan fallback at the end of a pass now names fn.freshPassQueue()[1] rather than the old list's first leaf; with a bucket queue it keeps list[1].

KNOWN COST: each pass re-pays the capped parents on the way down. On a piled realm that is one extra /who (1-90, then 1-89 / 90-90); on an evenly spread realm it is the halvings back down to the leaves.

Tests/zz_scan_queue_population_spec.lua gains five examples: the wrap rebuilds from the range, picks up a mid-pass range edit, re-queues custom scans with fresh flags (no stale flag survives), prices the new pass from the rebuilt queue, and freshPassQueue touches no scan state.

Fixed — rotation member rows were re-sorted by the active column, and the arrows never reached a tagged rotation's guild snapshot

Field report (Vishiswaz): "it looks like they DO work but they don't appear in their new order until a reload/relog". Two defects in GUI/Tabs/Announce.lua:

  • The sort. A header click collapsed every rotation (onSortChanged), but RowList kept the sort, and expanding a rotation while it was active let _getSortedData re-sort the spliced member rows by that column. Measured in the new spec with the old wiring and Name sorted descending: 1. Beta | Rot | Beta | Alpha | 2. Alpha, members scattered away from their rotation. A reload cleared RowList's sort, which is why the order then looked right. The tab now uses externalSort, the same arrangement Eligibility adopted in v2.13.0 for the same reason: buildRows(sortKey, sortDesc) orders the top-level rows (sortTopRows, RowList's comparator, Name on _sn) and splices members after, and a sort no longer collapses expanded rotations.
  • The guild snapshot. While guild announcements are enforced the expanded rows draw the policy snapshot, and the arrows only swapped the editor's own profile, so a tagged rotation neither moved on screen nor changed the order the guild posts until the next Save. The arrows now call reordered(rotId), which re-snapshots through refreshPolicySnapshotsFor, gated on the tag and not on permission as Save is (finding 9).

Tests/zz_announce_rotation_reorder_spec.lua (3 examples) drives the rendered tab: expand, click the real up arrow, and read the rows in pool (draw) order, for a personal rotation, under a Name sort, and for an enforced tagged rotation.

Fixed — an officer's Save stopped reaching the guild's filter once they had adopted another officer's policy, and the Filters row hid the difference

Field report (Vishiswaz, TBC): a guild filter wanting Paladin, Priest, Shaman and Warlock, and the chat log still showing 70-70 c-"Mage", 70-70 r-"Tauren" c-"Hunter" and a Rogue search. Tests/zz_class_filter_queries_spec.lua drives the real /who callback with those four classes, as the member's own filter and as the enforced guild copy, race-first and class-first: no unwanted class is ever queued. So the engine was sound and the filter it scanned with was not the one on screen. Two defects in GUI/Tabs/Filters.lua, found by reading:

  • Save. FiltersTab.RefreshGuildMarkFor refused unless policyIsOurs(). With two officers, the first policy one adopts from the other sets gmPolicyReceived, and every later Save of a marked filter updated only the officer's own copy. The gate is now policyIsOurs() or canEditPolicy(): an officer who may edit policy could already untick and re-tick the Guild box to the same effect, while an ordinary member is still refused. The login window where the rank index is nil still falls back to policyIsOurs().
  • The row. buildRows hid the enforced guild copy behind a personal row of the same name and drew the personal criteria. While guild filters are enforced the row now draws the copy the scan uses.

Tests/zz_filters_guildmark_spec.lua: the examples written for an ordinary member now set a non-editor rank explicitly. The harness's default rank may edit policy, so they had been running as an officer without saying so. An officer example is added. NOT VERIFIED that this was the state of Vishiswaz's guild.

Fixed — a welcome still went out to a member who had already left

Field report (Vishiswaz): "Goonbrain has left the guild" at 04:30:47, the auto-blacklist line at 04:30:50, "Welcome to the guild, Goonbrain!" at 04:30:52. The greeting waited in the click queue and went out on the next click regardless. Welcome:CancelFor(name) drops the leaver from the pending guild line (bare name) and the whisper queue (normalized full name), closing the window if nobody is left; GuildEvents:OnLeave calls it before the isNew gate, since the leave list may already hold the name from an earlier stint. Others in a combined line are still greeted. Tests/zz_protected_welcome_spec.lua gains both cases, through OnLeave.

Two gaps in that fix, confirmed by Peer Review (thread 1dd7a8fe, reply ec939e4d) and fixed with the remedy it gave:

  • The event-log route never cancelled. GuildEvents:ProcessEventLog concludes "they are gone" inside if not fn:isPlayerInMyGuild(name) and now calls Welcome:CancelFor(handoff or name) there too. handoff, not name: the log gives a bare name, the welcome was queued under LibGuildRoster's Name-Realm, and normalizePlayerName leaves a bare name bare on the Classic family, so my first version matched nothing there while its spec, which queued a bare name, stayed green (Peer Review re-check, reply b46cde32). The spec now queues the library's spelling. The queue is session-only, so this only reaches a departure from this session. KNOWN COST: a member who left while we were offline and rejoins now, before the roster cache lists them, can lose their fresh welcome when the log is read.
  • The guild line matched the bare name, so Name-RealmA leaving dropped Name-RealmB's welcome. PostGuildWelcome(chatName, fullName) now keeps the normalized full name beside the display name (pendingFull, every removal through removePending), FGI_Core passes it, and CancelFor matches on it; the bare name is used only for an entry queued without one.

Tests/zz_protected_welcome_spec.lua adds a same-name-other-realm example and a departure seen only in the event log.

Fixed — a member who received the guild policy could rewrite a guild announcement on their own client

Self-audit finding 2 (2026-09-16), unreviewed. AnnounceTab.RefreshPolicySnapshotsFor was gated on the tag alone (finding 9), but gmPolicy is one record every member receives, so the tag is on a plain member's client too. A member whose personal profile shares an id with a guild snapshot (a template carries announcement profiles as they are) rewrote their copy of the guild announcement on Save, or on a rotation arrow since this release, and then posted it in their own wording. It now takes the Filters tab's gate, PolicyIsOurs() or canEditPolicy(), with policyIsOurs exported from GUI/Tabs/Filters.lua as addon.PolicyIsOurs rather than copied. Finding 9's cases still refresh: the author during the login window, and a revoked delegate's own record. Tests/zz_announce_policy_spec.lua adds a receiving non-editor (refused) and a receiving officer (refreshed).

Fixed — the Filters row showed the guild's criteria with nothing saying so, and Scan Groups' next scan

Self-audit findings 4 and 1 (2026-09-16), unreviewed.

  • Filters row. A row showing the enforced guild copy loads the member's own copy when clicked, and a member who cannot edit policy only ever saves their own copy, so the row did not change and read as "Save did nothing". For that member the row is now named <name> (guild), like the read-only guild rows; an officer, whose Save refreshes the guild copy, sees it unmarked. The row click loads rawName. Tests/zz_class_filter_queries_spec.lua renders both cases.
  • One builder for a pass. ScanGroups.PopulateQueue's standalone bucket was a second copy of fn.freshPassQueue. That function now takes exclude (the scan names a group already runs) and builds the bucket; PopulateQueue is split into a side-effect-free ScanGroups.BuildBuckets plus activation; freshPassQueue gains the nil-range guard the copy had. The Scan tab's Next scan past the end of a bucket now names the next bucket's first query, or the first query of a rebuilt grouped pass, instead of the ungrouped queue. Tests/zz_scan_queue_population_spec.lua asserts the standalone bucket equals freshPassQueue minus the grouped names.

Fixed — "Assign to" in the Wingman right-click menu could not be opened

Field report (Vishiswaz): "doesn't seem to do anything on mouseover, isn't interactable". The row in GuildRosterTab.AddBatonEntries carried notClickable. Blizzard's UIDropDownMenu_AddButton turns that into info.disabled: the button is disabled, an invisible button covers it, and the expand arrow is disabled, so UIDropDownMenuButton_OnEnter, the only thing that opens a hasArrow submenu, never runs. Identical in the classic_era, classic, anniversary and live trees. The row now uses keepShownOnClick. The same row is on the Guild Roster tab's baton menu, which had the same fault. Tests/zz_wingman_step_menu_spec.lua asserts the row's flags; the harness does not model the dispatch itself.

Changed — "Wingman paused" says so once per instance visit, not every 30 seconds

Operator: "this is firing too often, make it less often". The overlay drains on every click, so the 30 s throttle still printed the line every half-minute of clicking for a whole dungeon. It now prints once when a paused drain first happens, and is re-armed by a drain outside instanced content or by Start. Tests/zz_wingman_instance_pause_spec.lua gains once-per-visit and next-visit examples.

Changed — the zone step searches every open-world zone, not only the zones the first 50 results stood in

Vishiswaz (TBC): "The subdivision by zone should be searching every single eligible zone, not just the ones that came up in Search 3." The operator reopened the 2026-09-14 sample-only decision: "change it the way he wants it". A capped "70 Troll Hunter" returns 50 players from a handful of zones, so a Troll Hunter anywhere else was never asked about.

fn.zoneFanOut now lists the zones the capped query's rows stood in first, in row order, then every other zone from fn.getWorldZones(), minus IsInQuietZone (the built-in instance list always, the user's own list while their toggle is on). The zone list is data, like the instance list: tools/instance-areas.lua now also writes FGI_WORLD_ZONE_AREAS into FGI_InstanceAreas.lua, every zone of every world map per flavour, and fn.getWorldZones resolves the ids through C_Map.GetAreaInfo so the names are the client's own in every locale. Phased copies of one zone collapse to one name. An empty resolve is not cached.

Three corrections to the generator's zone rule, found by reading the list before shipping it:

  • Capitals. A zone was a top-level area with sub-areas, and Classic Era's Orgrimmar, Ironforge and Undercity have none, so the busiest places in the game were missing on Era and TBC (and The Exodar on TBC and MoP). A top-level area carrying AreaTable flags 0x100 and 0x10 together now counts too, which every faction capital has in all four tables (312 on era/tbc; 2113848, 2097464, 69222712 later). The first cut tested 0x100 alone, and retail's Legion Dalaran is two dozen top-level rows with 0x100 but not 0x10 (Flags_0 18728): 23 fragments such as "Sewer Exit Pipe" reached the list, and the outdoor "The Violet Hold" (7895) un-silenced the dungeon of that name. A capital row with no sub-areas is also refused when a dungeon or raid of that flavour has its name. Silenced counts are unchanged (48/64/170/481).
  • The development map. Open maps with Directory "development" are skipped, so Programmer Isle is no longer a zone on Era and TBC.
  • Retail runs and story scenes. Single-zone open maps that are a run or a scripted scene cannot be told from Deepholm or Exile's Reach by any Map or AreaTable field (InstanceType, flags, loading screen and expansion all overlap), so NOT_SEARCHED_MAPS / NOT_SEARCHED_AREAS in the generator name them with a reason each: the two Horrific Visions, Torghast, Font of Fealty, Grand Time Adventure, Hall of Awakening, the old "Kul Tiras" row on Eastern Kingdoms and Faralohn, which never shipped. They only keep an id off the zone step; the instance list is untouched. Val and Naigtal look like these and are not: they are Midnight's weekly Showdown zones, so they stay.

Zones per flavour: Era 46, TBC 61, MoP 102, retail 216. KNOWN COST: one capped leaf now asks about every zone. On TBC that is up to 61 queries at the 5-second floor, about five minutes, and retail's 216 at 8 seconds is about half an hour. The progress bar steps back further when a zone split lands, because the split is still charged when it happens.

Not verified: that C_Map.GetAreaInfo returns exactly the string /who prints for these ids. The instance list rests on the same assumption. TrinityCore's WhoEntry carries an area id for the client to name, which supports it, but nobody has compared the two in a client.

Tests/zz_who_subdivision_spec.lua's zone examples were written for the sample and passed only because the offline client could name no world zone. They now name three Classic Era zones and assert the exact fan-out: sample first, then the rest, instances and the user's zones left out, another flavour's zone left out, and an empty answer not remembered. Tests/zz_instance_areas_spec.lua pins the capitals, the Outland zones on TBC only, Hillsbrad everywhere, no Programmer Isle, no Dalaran fragments, the retail exclusions with Val and Naigtal kept, and that no area is both a zone to search and an instance in the same flavour. FGI_WORLD_ZONE_AREAS is on the accepted-globals list.

Fixed — Hillsbrad Foothills was treated as an instance on TBC, MoP and retail

Follow-up to the same report ("it is doing SOME vanilla zones ... But not all of the time"). Reading the generated instance list turned up [267] = { tbc = 1, mop = 1 } -- Hillsbrad Foothills (dungeon). An instance map on those flavours names 267 as its own area, and tools/instance-areas.lua counted a top-level area as a zone only in an elseif after the instance check, so a row an instance claimed never reached the overworld refusal. Hillsbrad Foothills, with Tarren Mill and Southshore under it, was silenced: everyone standing there was dropped at ingestion and the zone step never asked about it. The zone test now runs regardless of the instance claim. Regenerated from the same 2026-09-14 builds: the only changes are new refusals, 267 on tbc/mop/retail and 616 "Mount Hyjal" on mop (a zone there). A doorstep with no sub-areas (Era's Onyxia's Lair) still fails the zone test and stays silenced. Tests/zz_instance_areas_spec.lua asserts Hillsbrad is silenced nowhere.

A full read of the regenerated table (the operator: "look at the zone tables again, to ensure they are accurate") found one more: 207 "The Great Sea" silenced on era/tbc/mop. It is a top-level row inside the Deadmines map and also the open ocean off both continents (332 on Kalimdor), so every player at sea was dropped; retail's 8276 "The Veiled Sea" scenario is the same shape. It is not a zone with sub-areas, so the rule above cannot see it. The generator's sea rule: a name that is any top-level area of a world map is refused when the row is a secondary region of its instance map (the map also carries a row that collides with nothing, "The Deadmines") or a scenario. Map.AreaTableID cannot identify the main row, because it is 0 on the Deadmines. The first cut refused every colliding non-own row, and that un-silenced Azjol-Nerub, The Violet Hold, Auchindoun, Tol Dagor and Tazavesh on retail, each the only row of its map with an outdoor doorstep of the same name; it was replaced before anything shipped, and the spec now pins those five. Net change from the sea rule: 207 refused on era/tbc/mop; retail refuses 8276, and scenario copies whose dungeon keeps the name silenced (7777 The Violet Hold, 10272 Tol Dagor, 8635 The Frozen Throne), 8538 Twisting Nether, 14770 Azmerloth, and 4896 "The Frozen Throne" wing of Icecrown Citadel. KNOWN COST: a player on the Lich King's platform in retail Icecrown Citadel is no longer silenced if /who reports that wing by name. The rest of the raid still is. The other outdoor-sounding names in the table (Starfall Village, Shadow Hold, Alcaz Island, Stromgarde Keep, Greenstone Village, Coldridge Valley) are sub-areas of a zone outdoors, so /who reports the zone and they cannot catch an outdoor player.

Fixed — the Dump window said filters were off, and its Filters button raised on a raid requirement

Found by reading lines the coverage sweep reported unrun. Both Filters readouts in Modules/dump.lua gated on DB.realm.enableFilters, a legacy master switch the scan no longer reads (soleAllowedClass: "no master enableFilters gate") and which defaults to false. So General said Filters: Off and Filters listed nothing while the scan filtered normally, which is the wrong answer in the window a player pastes when asked what their addon is doing. The one filter line it could print formatted the raid thresholds as rioRaid[0], [1], [2] with %d; they are saved at [1]..[3], so the nil [0] raised for any filter with a raid requirement.

Both now read fn.effectiveFilters(), the set the scan applies, and say (guild policy) when that set is the guild's. The filter lines print the v2 wantedClasses / wantedRaces and label a legacy filter's classFilter / raceFilter as exclusions. Tests/zzzz_dump_debug_windows_spec.lua (17 examples) covers these, the Areas list's own-zone marker, and both windows' drag-to-save, close button and login restore, with the login handlers reached through fireEventFiltered rather than a real PLAYER_LOGIN at every frame. Modules/debug.lua and Modules/dump.lua are now at 100%.

Fixed — every login cleared the invite and next-search hotkeys before binding them again

Found by reading FGI_Core.lua's unrun lines. On PLAYER_ENTERING_WORLD, purgeStaleClickBindings ran ahead of fn.applyKeybinds and cleared any configured key whose action was CLICK FGI_CompactInviteBtn or CLICK FGI_CompactScanBtn, then saved bindings.wtf. It was written in v2.2.1, when those were stale pre-v2.1.8 bindings; since v2.2.4 they are exactly what applyKeybinds binds invite and next-search to. So every login and /reload deleted the player's two working keys, saved that, and applyKeybinds bound them again a moment later. Deleted as wholly redundant: the purge only looked at the three configured keys, and applyKeybinds rebinds each with SetBindingClick straight after. (Whether applyKeybinds' GetBinding(i) sweep sees user CLICK bindings is not established; the v2.2.1 note said it does not, and nothing relies on it.) The same function's if GetNumBindings and GetBinding guard is gone too: Blizzard's Keybindings settings page calls both bare in all four flavour trees.

Also from reading OnEnable: it cleared the debug window's anchors and pinned it TOPLEFT, while Modules/debug.lua's PLAYER_LOGIN handler owns that position and centred the window without clearing, so a player with no saved spot could be left with two anchors depending on which login handler ran first. OnEnable no longer touches the window, and debug.lua clears before centring. Neither fix has been seen in a client.

Tests — six examples for the parts of this release that had none

Asked by the operator ("did we write tests for all the changed things"), and the honest answer was no. Added: the zone split's progress charge with the full zone list in play, at the default order and zone-first (zz_who_subdivision_spec); the welcome queue's full names staying in step after a peer's welcome removes the joiner before them (zz_protected_welcome_spec); fn.freshPassQueue with no level range at all, and the Scan tab's Next scan at the end of a group and after the last group, rendered and read (zz_scan_queue_population_spec); a click on a (guild)-marked Filters row loading the member's own filter (zz_class_filter_queries_spec). Reasoned to fail on the old code, apart from the zone charge, which covers a wider split than before rather than a change; not run against it. Suite 3040/0. Review of the whole session requested from Peer Review (thread e35ca0ba), not yet answered.

Re-measured the files this release touched that were at 100%: Modules/FGI_ScanGroups.lua, Modules/FGI_Welcome.lua and Modules/FGI_GuildEvents.lua still are. functions.lua had dropped to 3107/3108 on one line not from this release: the presence guard in fn.installConversationTabSuppressionHook on hooksecurefunc, FCF_OpenTemporaryWindow and CHAT_FRAMES. All three exist before any addon on every flavour that reaches it (Blizzard_ChatFrameBase_Classic.toc is LoadFirst: 1 in the classic_era, classic_anniversary and classic trees; retail returns earlier), and only a spec deleting CHAT_FRAMES ever reached it. Deleted with that citation; back to 3105/3105.

Tests — coverage: debug.lua, dump.lua, FGI_Placeholders.lua and FGI_Constants.lua to 100%

The four cheapest files left on the coverage list (61%, 75%, 70%, 91% on 2026-09-12). debug.lua and dump.lua are covered by Tests/zzzz_dump_debug_windows_spec.lua, which also carries the Dump window fix above. Tests/zz_placeholders_guildlink_spec.lua (13 examples) drives the club-finder link lookup (no API, no club, no listing, a listing), the whisper GUILDLINK path (cached after one lookup, spaces made unbreakable, refusal with a chat line, the G_LINK marker), and the estimator's whisper and split modes (cached link, 120-byte allowance, GUILD, one message, N chunks against the real fn:messageSplit, the unsplittable faildrop). Tests/zz_locale_derived_tables_spec.lua gains four examples for the derived-tables net, reached through a specs-only FGI._guaranteeDerivedTables because it runs once at registration: nothing missing, the empty-table repair, and each of its two messages.

Deleted rather than covered, each after reading the Blizzard source or the callers:

  • FGI_Placeholders.lua resolveGuildLink's plain fn.fetchGuildLink() fallback for a client without securecallfunction. Blizzard_SharedXMLBase calls it at load in the classic_era, classic_anniversary and classic trees (SecureTypes.lua, MathUtil.lua), so no client lacks it.
  • FGI_Constants.lua FGI:RebuildLocale(code), the back-compat shim for the pre-LibLocaleOverride builder: nothing called it. Locale/nlNL.lua's header named it as the fallback mechanism and now names LibLocaleOverride.

Suite 3070/0.

Tests — coverage: FGI_Core.lua to 100%

254 lines were unrun (74.9%). The slash specs had called them unreachable because FGI_Core.lua binds its DB in OnInitialize, but in the whole suite an earlier file's PLAYER_LOGIN already runs OnInitialize through AceAddon. Tests/zzzz_core_spec.lua (57 examples) delivers PLAYER_LOGIN to AceAddon's frame alone, which is a no-op once the queue has drained, and reads the upvalue back off Console.FGIInput. It covers the minimap launcher (main, compact show/hide/not built, settings, control-click group invite), the chat menu's Black List entry (fast, prompt then advance, not the queue's head), the officer !fgi and !blacklistGetList replies, the join handler's welcome and retail accept arms, OnEnable (no SetItemRef hook, version check, auto-kick, slash registration, guild name), addon.MigrateRealmStores, fn.applyKeybinds (bind, sweep, save, debug line, combat deferral) and its entering-world frame, the three keybind buttons, and /fgi debug, blacklist, factorySettings, resetSettings, resetWindowsPos, compact, kbstate and the rest of template.

OnInitialize runs once per Lua state (LibDBIcon refuses a second Register), so its two pieces with several answers moved into addon.ReportLoginTemplate and addon.OpenDebugSession, like addon.MigrateRealmStores before them. OnEnable had raised at its SetItemRef hook in every earlier suite run, because the harness did not model that FrameXML global; requested and delivered the same evening (harness d0f7343). The hook itself was then deleted (next section). Modules/debug.lua stays at 100% after the fix above (its two redefined frame locals renamed).

Fixed — hovering a player from a connected realm showed a same-named local player's blacklist, invite and join lines

Modules/FGI_ChatTooltip.lua looked every store up by the link's name and then by the name with its realm cut off, whatever that realm was. So hovering Bob-SisterRealm read a local Bob's blacklist reason, invite stamp, race, join record and leave flag, and the roster fallback could show the local Bob's rank and level. The stores are now read through fn.playerKeyed, which is keyedUnderEitherSpelling in functions.lua returning the record instead of true: it drops a realm only when it is the client's own, the rule the four invite gates already followed. The tooltip's roster fallback takes the same rule. A blacklist record with no reason also printed Blacklisted: table: 0x...; it now says Blacklisted.

The same raw index was in two more places, both fixed through fn.playerKeyed. The race backfill (Modules/FGI_RaceBackfill.lua) matched a /who result's bare name against join records keyed by the roster's spelling, so it reported same-realm guildies as "not in FGI's stores"; and MemberHistory:onJoin never carried the race of a recruit the scanner had filed bare. The backfill's "already had race" count also compared the running fill totals, so once anyone earlier in the results was filled nobody later was counted; it is per player now.

Fixed — /fgi help said /fgi requeue spends scan queries, and the README and CurseForge page described features that no longer exist

Locale/enUS.lua helpRequeue still read "Re-check queued candidates with a /who each (spends scan queries)", untrue since the v2.12.1 presence sweep. It now says the check spends no scan queries.

A full pass of README.md and docs/Curseforge_Description.html against the code, each claim read in its function. Removed as not in the code: a "Quiet List" of players, a priority "Custom List", invite testing mode (no setting reaches testingMode), the filter-overwrite confirmation, the "Remember All" setting, zone filters and a ChatThrottleLib claim for whispers (they call the chat API directly). Corrected: the invite modes (Whisper + invite sends both on one click; Whisper only is a private whisper, not a broadcast), race/class filters (include only), several filters on at once (any one accepting keeps a player), the %player% placeholder (it is NAME; GUILD and retail GUILDLINK were missing), the anti-spam window (days, 0 = forever), the Anti-Spam tab (X and Re-check & invite, no right-click), "Auto-kick on blacklist" (the guild check asks Kick / Remove / Skip), scanning "in the background" (every /who is one press), the scan engine's queue working "automatically", the Race/Class table's source, the Eligibility tab's account rows, the minimap clicks, settings paths (General → Scanning, Advanced → Sync, Guild Policy for duty messages), and the README's claim that Recheck does not advance Seen (FGI_Presence.lua records a Seen day for an online answer), the README Quickstart's "set ... zone" on the Filters tab, the README's zone step (it still named only the zones the first 50 were in, before this release's every-open-world-zone change), and the Statistics totals (they follow the chosen period, all-time only for All). The README's CurseForge link is gone: Discord is its only external link. Pixel widths for hidden counters were dropped rather than recomputed. Added to the page: batch selection, the Last/Next scan line, level range source, held candidates and Guilded, per-filter messages, Custom Scan Strict and Default Scan, Scan Groups, the soundboard, policy master switch and extras, the blacklist search, the History / Statistics / Guild Roster tabs, member-history and chat tooltips, race backfill, recruit-reply alerts, Only my invitees, the Invite Group window, hotkeys, chat noise, notifications, reopen-after-reload, the Legacy window and Scan logs; and /fgi roster list and /fgi help2 to both slash-command lists.

Found and left for after the release: in-game help text with the same drift (the Race/Class tab help says "maintained by hand", the History tab help describes the old per-invite log, the Quiet Zones help says scanning pauses while you are in a zone, a welcome message names the removed "Send welcomes on a click" setting, a subdivision tooltip says "halve the range", and the Anti-Spam tab points at a "Clear DB after" setting that no longer exists). Their keys are the English text, so each change touches all 31 locale files.

Changed — code no client could run, deleted after reading the flavour trees

  • The chat-menu fallback. FGI_Core.lua's hooksecurefunc("SetItemRef", ...), the follow-on dropdown it opened (ChatMenu.ShowFollowOn), the _registeredModern flag that decided it and ChatMenu.PrintDiagnostic. Blizzard_Menu.toc is LoadFirst and loads Menu.lua for every game type in all four trees, and Menu.ModifyMenu only asserts its tag is a string, so registration always attached. The per-tag pcall, the Menu presence check and attachMenuTooltip's SetTooltip check went too (MenuUtil.lua:84-93 mixes SetTooltip into every button), as did the SetText/AddLine fallbacks for GameTooltip_SetTitle/GameTooltip_AddNormalLine, which are SharedXML everywhere.
  • Modules/FGI_UnitTooltip.lua's four retail pcall wrappers. enrich is registered only when not gv.isRetail. Both registration branches stay: TooltipDataHandler.lua is [ExcludeLoadGameType vanilla], so Classic Era takes the OnTooltipSetUnit hook and TBC/Mists take TooltipDataProcessor.
  • GroupInvite:Close() (no caller), fn.backfillRacesFromCache (no caller), and presence checks in the group-invite panel, race backfill and chat tooltip on modules that load ahead of them in every TOC.
  • Four single arms: history.lua's history.invites migration (the key is in the AceDB defaults, so it is never absent); FGI_Dialog.lua's insetWindowBackground fallback (functions.lua defines both); init.lua's bare GetAddOnMetadata arm (C_AddOns.GetAddOnMetadata is in all four trees); FGI_Compatibility.lua's "Unknown" name (the fallback always sets a flavour) and its FGI_DEBUG.versionDetection print (nothing writes that key).

The keybind event frames moved to file-scope upvalues, and combat now reuses one re-registered frame. A frame is never freed in the client, so every keybind edit made in combat used to leave another frame that re-applied the bindings when combat ended; offline, a frame reachable only through its own script could be collected, which made the entering-world example pass or fail on collector timing.

Tests — coverage: the chat menu, both tooltips, the group-invite panel and the race backfill to 100%

FGI_ChatMenu.lua 94/94 was 36/184, FGI_ChatTooltip.lua 148/148 was 28/170, FGI_UnitTooltip.lua 50/50 was 17/72, FGI_GroupInvite.lua 113/113 was 16/125, FGI_RaceBackfill.lua 84/84 was 11/93; history.lua, FGI_Dialog.lua, init.lua and FGI_Compatibility.lua reach 100% by the deletions above; functions.lua, FGI_Core.lua and FGI_MemberHistory.lua stay at 100%. Every file in the first measured chunk of twenty is now at 100%. New specs: zz_chat_menu_spec (11), zz_chat_tooltip_spec (12, including the connected-realm examples), zz_unit_tooltip_spec (9, reloading the module against each hook API), zz_group_invite_spec (6, the real panel and RowList rows), zz_race_backfill_spec (9, including the per-player count), and one example in zz_member_history_spec. Two of them pin RealmResolver.Resolve to unknown: its name cache outlives spec files, and an earlier file's Bob decided which realm a bare Bob got.

Harness requests: Menu.ModifyMenu with GameTooltip_SetTitle/GameTooltip_AddNormalLine (delivered as ae94ed6), after a local stand-in in Tests/support/addon.lua that is now deleted. Whole suite 3175/0.

Tests — harness pin moved to 7d63d11

From 77cba8f, through 1af5f84. Carries SetItemRef, the Menu model and the tooltip helpers above, the raw CHAT_MSG_ADDON sender as Name-Realm for same-realm senders, GetNormalizedRealmName keeping apostrophes, Ambiguate following its documented contexts, and frames.reset() raising on a handler added mid-dispatch. Every Adoption entry since 77cba8f asks for nothing or a suite run; the suite is green with no spec changes beyond deleting the two stand-ins.

Tests — harness pin moved to 77cba8f

34107ac (mouse propagation on both frame models) and d09f6df (FriendsFrame starts hidden) among them. Full suite green with no spec changes; the Wingman specs' SetPropagateMouseClicks stand-ins only install when the method is absent, so they are now inert.

Tests — the /who harvest count now counts only LibWho's reads

Tests/zz_who_result_count_spec.lua counted every C_FriendList.GetWhoInfo call, and LibGuildRoster-1.0 (a real sibling library the suite loads) now also reads each row on WHO_LIST_UPDATE for its sister-guild sightings, so every count doubled (100 for 50, 14 for 7). The counter now counts a call only when its caller's source is LibWho.

Tests — the "client without click pass-through" Wingman example never modelled that client

Tests/wingman_prompt_spec.lua removed the propagation methods with = nil to stand in for a client that lacks them. Frame methods sit on a shared prototype reached through __index, so nil only drops an instance override and the read falls through: to the hollow layer's truthy no-op at the current harness pin, and to the real method from harness 34107ac on. The example passed only because the no-op getter returned nil. The methods are now set to false, which shadows the prototype and reaches Wingman.lua's not overlay.SetPropagateMouseClicks branch as intended. Raised by WoWAPITesting before the pin moves.

[v2.13.3] (2026-09-14) — a capped level band is cut where the players are, not in the middle

Changed — a level band that comes back full is cut at the level the sample is piled on, so a max-level realm reaches 90-90 in one split instead of seven

Field report (Spacedoc, Discord, 2026-09-14), retail: "if you have 15 people on levels 1-89 and 50+ on level 90 ... it is around 6-8 [splits]". Traced through the arithmetic halving it was exactly seven: 1-90 → 46-90 → 68-90 → 79-90 → 85-90 → 88-90 → 89-90 → 90-90, every one a capped /who returning the same level-90s again, plus the seven low halves each of those left — fifteen level-only queries before the race/class fan-out that actually finds anyone could begin. The reporter's count was right and the answer given on Discord ("three, maybe five") was not.

The cut now comes from the rows. The 50 rows a capped query returned are a sample of who is standing in the band, and fn.levelSplitPoint(a, b, rows) cuts at the sample's upper median level when that level holds at least half the sample — a pile — and halves the band as before when it does not. For the reporter's realm the median is 90 and the first cut is 1-89 / 90-90: the 1-89 half returns the levellers in one uncapped /who, and the seven splits are one. The upper median rather than the lower so a sample split evenly between 89 and 90 cuts between them; a pile sitting on the band's floor is isolated the mirror way (1-1 / 2-90). Rows outside the band are ignored and no usable row at all means the old middle. No setting, no new tag, nothing for a user to configure — the same algorithm, cutting in a better place.

The pile guard is measured, not cautious, and it is the part worth reading. Nobody documents which 50 a live /who returns when more match, so Tests/zz_level_split_point_spec.lua (29 examples) does what the Discord thread could not: a fake /who server over four synthetic realms, the whole scan drained through the real registered callback under both cut rules, under three server orderings (shuffled, lowest-50-first, highest-50-first), and every /who counted. An unguarded median was worse than halving on an evenly spread realm whenever the server hands back the lowest 50 — 37 /who against 23 — because the sample then describes only the bottom slice of the band and its median peels the band off four levels at a time. With the guard the spread realm is the old halving query for query under every ordering (asserted), and the piled realms come in at or under it everywhere:

realm server returns halving this
retail-shaped 1-90, 40 levellers + 1,500 at cap shuffled 32 20
lowest 50 first 31 21
highest 50 first 31 19
classic-shaped 1-60, 120 levellers + 600 at cap shuffled 29 23
lowest / highest first 30 24
evenly spread 1-90, 400 players any 23 23
starter pile 1-90, 300 at 1-10 + 40 above shuffled / lowest / highest 70 70 / 64 / 68

The progress denominator is untouched: a band of n levels is priced n·leaf + (n−1) and two halves of n levels price to the same number wherever the boundary falls, so the conservation the subdivision spec asserts holds by identity — pinned at an off-centre cut. The descending-sweep setting is applied after the cut as before.

Docs — the scan's "how a full search narrows" is now described where the features are

README.md never said how a search gets past the 50-result cap at all; the HTML said it in one bullet. Both now carry the four-step narrowing (level, race, class, zone), that its order is a setting, and that a full level band is cut where its players are. The HTML's guild-integration line also lists MoP Classic, which it had left out.

[v2.13.2] (2026-09-14) — every officer online announced the same leaver, the scan still reached players inside instances, a grouped welcome short-changed the last joiner, and on Classic a recruit's reply never reached their record

Fixed — with several officers running FGI, a member leaving was announced to officer chat once per officer

Field report (Vishiswaz, TBC): "more than 1 person online with fgi in officer chat causes multiple o chat messages for people leaving the guild" — two identical <FGI> Player Koje-Dreamscythe has been blacklisted. Reason - Left the guild lines, one from each officer's client, a second apart. Reported as a regression, and it is one from the reporter's side: v2.13.0 made the auto-blacklist route work on Classic for the first time, so the duplication it always had became visible.

Every client running FGI is in the same OnMemberLeft callback for the same leaver, and every one of them called fn:blackList, which posts the officer line unconditionally. The entry landing on every client is right — it is the same entry, and DeltaSync reconciles it — but the line has to have one sender. fn:blackList takes a fourth argument, auto; both departure routes in Modules/FGI_GuildEvents.lua pass it, and an automatic add posts the officer line only from the elected announcer (Recruiter:CanPostAutoNotice, the announcer election under a new name — the same list Q4 gave the welcomer, and for the same reason: it is who speaks for the guild, and every client computes it from the same policy and roster). A person's own /fgibl or Blacklist-tab add is unchanged — one person, one line. KNOWN COST, and it is the same shape as the welcomer's: a guild running no policy at all still falls open, so several officers with FGI and no announcer list still each post. Falling closed there would silence the line for every guild that never configured a policy, and in the commonest case — one officer running FGI — falling open is one line and correct.

Found beside it: the officer line was worded from the UI-language override (L) while the chat print next to it used the client locale. Both the add and the removal line now go through fn.chatLoc(), the rule every other line into somebody else's chat frame follows.

Tests/guild_events_spec.lua pins that both routes pass the flag; Tests/zz_functions_spec.lua (new block, six examples) drives the real fn:blackList: a manual add posts whatever the election says, an automatic add posts only when elected, the entry lands either way, no election module means post, the setting off means silence, and the wording comes from the client locale.

Fixed — the list of places that count as an instance is generated from the client's own data, for every flavour, and the zone step no longer runs a /who into one

Field report (Vishiswaz, TBC): "fgi still searching in instances" with z-"The Botanica" on screen. The operator's reading of it, verbatim, which is the whole of the fix: "filter out anyone in a dungeon/instance/raid/etc from the who results so you don't invite someone in a protected state" — and on where the data comes from: "you have to get that data for EVERY flavor of the game and maintain it for EVERY flavor of the game. it's a pain, but we have to do it as the API isn't working well."

The quiet-zone set was never complete, on any flavour. It was 45 hand-typed area ids (fewer instances than vanilla alone has) unioned with a walk of the client's Dungeon-typed maps — a walk that finds nothing on Classic Era, which files its dungeons as ordinary Zone maps, and that on every flavour depends on a map-type flag meaning something it does not quite mean. So a /who row standing in an instance the set did not name passed the gate, was queued, and was invited.

The set is now generated. FGI_InstanceAreas.lua is every top-level area of every instanced map in the four flavours' own DB2 tables (Map.InstanceType ~= 0 joined to AreaTable, plus the area each instanced map names as its own — Classic Era's Onyxia's Lair row says it is on Kalimdor and only the raid map's own pointer catches it), produced by tools/instance-areas.lua from wago.tools exports and regenerated per patch; the directions are in the tool. 511 ids across Classic Era (49), TBC (66), MoP (173) and retail (489). fn.getStaticAreas resolves this client's ids through C_Map.GetAreaInfo, which is what makes it right in every locale, and counts what resolved; the Dump window's Areas button prints the count. The Dungeon-map walk stays as a second source, unioned as before.

Each id names the flavours that silence it, and that is not decoration. A name is not the same place in every game: Zul'Aman is the raid on TBC and MoP and, since Midnight, a whole levelling zone on retail; area 206 is a "Westfall" inside the Deadmines on Classic Era and Utgarde Keep on MoP and retail. The generator refuses to silence any name that is also an overworld ZONE in that flavour — a top-level area with sub-areas, on a WORLD map — and prints every refusal so the decision is auditable. What a world map is took four cuts, each learned from a wrong one: any top-level overworld row (dropped the outdoor doorstep rows of Blackrock Depths, Onyxia's Lair, The Violet Hold, so the very dungeons the list exists for fell out); any open map (retail's Death Knight campaign phase is one open map holding one "Scarlet Monastery" with sub-areas, which made the real one scannable); two or more zones (made Deepholm, one zone on its own map, a non-world, so a scenario copy silenced the real zone); a loading screen (retail's story phases of Icecrown Citadel, Firelands and Ny'alotha have one, so the raids fell out). A world is an open map holding two or more zones, or one zone plus a loading screen with no dungeon or raid of that name — a single-zone map named after a raid is that raid's story phase; a single-zone map only a scenario copies is the real place. KNOWN COST: a recruit standing on an instance's outdoor doorstep is silenced with the instance (they are about to be inside anyway); on retail Eye of Azshara, Tol Barad, Ashran and Wintergrasp share their name with a real zone and are therefore not silenced there; and Torghast is refused because its lobby is an open map of that name, so its runs are scannable.

And the zone step reads the results through the same gate. The fan-out over a capped query's zones is still the sample — the zones the 50 rows were standing in, as it has always been — but the sample is now filtered by IsInQuietZone, the predicate the rows themselves go through, so an instance a row stood in never gets a query of its own. That is the z-"The Botanica" line.

The instance list is baked in; the Quiet Zones toggle is the user's list only. The operator, verbatim: "the 'built in instance list' is NOT part of the quiet zone configuration, it's baked in." Until now IsInQuietZone read DB.global.quietZones over the whole union, so switching the toggle off let players inside dungeons through and searched their instances. fn.getAreas now tags each name "instance" or "custom"; an instance is skipped unconditionally, a custom zone while the toggle is on, and a name on both lists is an instance (the user cannot opt a dungeon back in by adding it to their own list). The Quiet Zones tab's master-switch tooltip is rewritten to say so (enUS; the 30 other locales carry the old key and fall back to English for that one tooltip), the Dump window's Areas readout marks the user's entries (your list), and fn.isInstanceArea is the public form of the question. Pinned in zz_add_new_player_spec (an instance is skipped with the toggle off, and stays one when added to the user's list), zz_who_subdivision_spec (no query into an instance with the toggle off; a custom zone is searched again once the toggle is off) and the end-to-end block below.

Reverted the same day, before release: a draft of this fix had redesigned the fan-out to union every Zone-typed map in the client's tree behind the sample (~55 queries per capped leaf on TBC). The operator: "i didn't need you to change the design." fn.getZoneMaps and the tooltip that described it are gone; the tooltip says what the step does.

Tests/zz_instance_areas_spec.lua (new, 23 examples) pins the generated table's shape, the eleven instances the two TBC reports named silenced on TBC — including the seven Vishiswaz marked with question marks, driven end to end as /who rows through the real registered callback on a TBC client: none is queued, the Nagrand control is, the toggle changes nothing, and a client that cannot name them queues all twelve — the seven vanilla dungeons and the Season of Discovery instances on Classic Era, Zul'Aman silenced on TBC and MoP but not retail, Blackrock Depths everywhere, Deepholm and Westfall nowhere, Icecrown Citadel despite its story phase, the flavour a client reads (Wrath and Cata read MoP's column, the nearest data), fn.getStaticAreas per flavour and its count, and the Quiet Zones tab's PvP group derived from the same table and filtered to this flavour. Tests/zz_who_subdivision_spec.lua (block rewritten, seven examples) drives the real callback: a sampled instance gets no query whatever the toggle says, only the sampled zones are asked about, a sampled zone the map does not name is kept, the custom list is honoured while the toggle is on and searched again when it is off, and a batch standing entirely in quiet zones answers no axis.

Fixed — a grouped welcome went out on the first joiner's clock, so a late joiner could be greeted before they could read it

Field report (Vishiswaz, TBC): "the welcome delay is to make sure that people can see their welcome message … when grouping up multiple joiners together, the timer goes based off the first person invited in the group, not the last, which can result in the last person not seeing their message." Exactly right: PostGuildWelcome set the window's due time once, from the first joiner, and a rider joining in its last second was greeted one second later. A late joiner now pushes the due time out to at least the welcome delay from their join; the window still closes on time when nobody joins late, and the ten-minute life is still counted from the first joiner, so a stream of joiners cannot hold a welcome open forever. welcome_spec pins both.

Fixed — on Classic, a recruit's first reply opened a second History record instead of marking the attempt "replied"

Found by the coverage pass on Modules/WhisperAlert.lua, which had never had its alert path driven offline (17 of 71 lines). The reply was recorded as fn.recruits:onReplied(key) with key the realm-qualified fn:fullPlayerName(sender) ("Zugar-Realm"), while the attempt is counted in fn:invitePlayer as onAttempt(playerName) with the raw /who name ("Zugar"). The roll-up keys every record through fn:normalizePlayerName, which is the identity on every flavour but retail — so on Classic the two spellings were two records: "Zugar: 4 attempts, tried" beside "Zugar-Realm: replied", and the one line the History tab exists for was never written. Retail resolves both spellings to one qualified key, which is why no field report ever named it.

The alert now hands onReplied the raw sender, exactly as the attempt does. Tests/zz_whisper_alert_spec.lua (new, 19 examples) pins it with an attempt followed by a reply and one record at the end; the rest of the file drives the three cues, their three settings, the deferred chat-tab flash (matched case-folded because FCFManager_GetChatTarget upper-cases a whisper target, FloatingChatFrame.lua:2286), the login-burst window and the lockdown gate.

Fixed — a click during an encounter dropped a joiner's welcome whisper for good

Modules/FGI_Welcome.lua's chat-messaging-lockdown check sat inside sendWhisper, which the click pump called after removing the entry from the queue — so the whisper a recruit was owed went nowhere if the recruiter's next click landed inside a Mythic+, and the comment beside it still described the timer era. The pump now asks API.InChatLockdown() once per click and holds the entry, unsent and still queued, for the first click after the encounter, inside its ten-minute life like any other. welcome_spec pins it: nothing goes out during lockdown, the count stays at one, the next click sends.

Tests — thirteen more modules at 100%, and the branches that were deleted instead

The method that closed functions.lua — read each uncovered branch against the Blizzard trees before driving it — over the logic modules. At 100% now: WhisperAlert, RecruitGoals, AnnounceSync, MemberHistory, RealmResolver, Welcome, GuildEvents, AltGroups, WidgetPool, Baton, SettingsProfile, Presence, intro; Recruiter, DeltaSync and Wingman driven to their last lines (re-measure pending — the coverage tool timed out twice on the box while the harness ran its own gate). Suite 2873/0.

DeltaSync (Tests/zz_deltasync_bridge_spec.lua, new): the host callbacks driven at the bridge with a recording host — OnSyncAccepted requests the delta with our baseline, a refused send and a version broadcast reach the debug log, an untrusted query is dropped, a merged anti-spam entry leaves the live scan queue and frees the P2P session slot; /fgids and /fgids sync; the MINOR-15 floor refusing an older DeltaSync in chat; the single-revision and stored-canon arms for a DeltaSync without MakeHashEntry / ComputeHashV2; the recruits item stamped by statusAt. IsTrustedSender is one line now: LibGuildRoster's IsInGuild answers a boolean on every path (its own pre-build arm scans the live roster), so the hand-rolled roster scan and the "can't verify, allow" fallback behind ~= nil could not run — and the second would have opened the gate.

Wingman (Tests/zz_wingman_session_spec.lua, new, plus the prompt and step-menu specs): the configured step order and its fallback, the announce step (posts, falls through when nothing posts, skipped in lockdown), the protected-state pause, auto-stop, the scope line's name caps, the lock tooltip, the overlay's own click (pump first, and nothing over FGI's own windows); a start inside combat that arms and finishes on PLAYER_REGEN_ENABLED, keeping its duty provenance; cancelling while armed and the toggle debounce; the session restore and its retries, and the first duty evaluation waiting out the warmup — all through the load-time timers the prompt spec already recorded; the step menu's tick writing the setting and repainting, and its outside-click close; the auto-stop under a policy that bars both roles.

Deleted as unreachable on every flavour, not covered, each read in F:\Blizzard API Docs per flavour tree: the UIFrameFlash / FCF_StartAlertFlash / CHAT_FRAMES feature-detects in WhisperAlert (Blizzard_SharedXMLBase is AllowLoad: Both, Blizzard_ChatFrameBase is LoadFirst: 1); five no-C_Timer fallback arms across WhisperAlert and FGI_Presence (every UITimerDocumentation.lua declares it); Presence's "client does not expose C_ChatInfo.SendAddonMessage" refusal; WhisperAlert's inner canaccessvalue(sender) retest and pcall(fn.fullPlayerName), and RealmResolver's inline lockdown return — all three sit behind API.RegisterChatEvents' wrapper, which has already asked about every argument and returned during lockdown before the body can run; MemberHistory's addon.API feature-detect; SettingsProfile's locale() pcall (FGI:GetLocale is in the first file of every TOC); Recruiter's listForRole "unknown role" arm (every caller passes isRole first); AltGroups' two identical return false arms. Deleted as code with no production entry point: RealmResolver.NoteFullName and .Count, Recruiter:AssignedMap (superseded by AssignedWire at S48).

WhisperAlert reads addon.DB live rather than a snapshot taken at PLAYER_LOGIN: /fgi resetDB replaces the table, and the snapshot would have kept gating the alert's three settings on the discarded one.

A suite leak, fixed: Tests/zz_functions_spec.lua's conversation-tab block set CHAT_FRAMES, ChatFrame7 and ChatFrame8 to nil in its teardown, written when the harness modelled none of them. The harness owns all three now, so every later spec file ran with no chat-frame list and two dead frames — invisible until the first file walked the array, which failed in the full suite and passed alone. The block now drives the harness's real FCF_OpenTemporaryWindow / FCF_Close and stands in nothing.

The harness pin moves to 2c7279f. UIFrameFlash, FCF_StartAlertFlash, GetNumGroupMembers, IsInRaid and UnitFullName were raised (inbox thread 02b2da6bd297) and delivered the same evening; the stand-ins are gone, and the whisper-alert spec asserts the overlay's visibility and alpha on the clock and the chat tab's alerting flag rather than the call. One specs-only entry point came with it: fn.forgetConversationTabSuppressionHook, because the addon hooks FCF_OpenTemporaryWindow once per session behind a guard and the harness reinstalls that global per spec file, which drops the wrapper while the guard stays set. Nothing in the addon calls it.

Fixed — closing the Settings panel could take the compact tray with it, because the visibility fallback read the wrong SettingsPanel

GUI/SettingsPanel.lua's ShouldBlockEscClose — the check both ESC proxies ask before hiding their window — has a flag for the panel being open and a fallback that reads the panel's own visibility "in case the hook didn't work". The fallback was spelled (Settings and SettingsPanel) or InterfaceOptionsFrame, and inside that file SettingsPanel is our module table (line 595), which has no IsShown, so the read fell through; and InterfaceOptionsFrame is declared in none of the four Blizzard trees, so the second half never resolved either. The fallback answered false on every flavour since it was written. It reads _G.SettingsPanel now — Blizzard_SettingsPanel.xml declares name="SettingsPanel" in classic_era, classic, anniversary and live alike. Not run in a client: whether the flag path already covered every close on Classic, so the fallback never decided anything, is exactly what this could not tell. Found by reading the function to drive the compact tray's ESC proxy.

Tests — the compact tray at 100%, its handlers driven for the first time

Modules/compactFrame.lua measured 590/764 on 2026-09-13: three geometry specs had pinned its layout to the pixel and never pressed a button. Tests/zz_compact_frame_spec.lua (new, 48 examples) drives every handler the way the client would — GetScript and call — the eleven tooltips (including the gate that silences all of them, Wingman taking over the >> and invite tooltips, the help icon's 500 px width hint, the broadcast horn's Ready / countdown / activity rows), the three drag handles ending in one TOPLEFT save, the corner resize saving its width and refusing the button row's floor, expand / close / gear (with the next-frame re-show) / Wingman (right-click menu, warmup hold, toggle) / both horns, the warmup countdown overlay, the picker's own drag, resize and wheel and its name-tie order, the queue rows filled class-coloured and sized, the wheel clamped to the list, the four row icons including blacklist's fast-vs-confirm split, the ESC proxy's two refusals, and the login restore of a saved position and width, a junk save, and the reopen opt-in. 753/753 after.

The login restore is cf.restoreSavedLayout() now, called by the event handler, so a spec can drive it with a chosen save: the event frame is an anonymous file-local that unregisters itself on first delivery, and frames.fireEvent reaches every registered frame in the suite.

Deleted as unreachable on every flavour: the three elseif SetMinResize fallbacks (tray bounds, picker build, picker floor) — SetResizeBounds is in every flavour's SimpleFrameAPIDocumentation.lua and SetMinResize in none; makeRowIcon's Interface\Icons\ TexCoord crop — all four row icons are Buttons/ and RaidFrame/ art. GUI/UI.lua:71 and GUI/LegacyMainWindow.lua:318 carry the same SetMinResize fallback and are left for the GUI pass.

One harness gap, raised and delivered the same hour: _G.SettingsPanel was built shown where Blizzard_SettingsPanel.xml says hidden="true", so the fixed fallback blocked every ESC-close example offline (inbox thread 861ddf9e). The harness pin moves to 82eb99e (contains 557390a, the panel hidden after every reset); the before_each that hid it by hand is gone. Whole suite 2922/0 at the new pin.


Older releases have been moved to CHANGELOG_ARCHIVE.md — v2.13.1 and earlier, in full. v2.13.0 went section by section while it was the only version in this file (ten on 2026-09-09, twelve on 2026-09-10, eight on 2026-09-11, four on 2026-09-13, four on 2026-09-14, ten on 2026-09-16), and its heading and last section followed on 2026-09-16. v2.13.1 moved whole on 2026-09-16. The next candidate is v2.13.2, whole.

This mod has no additional files