From 01fafe7b371d799fe7599fbed011ed873dfa5aa0 Mon Sep 17 00:00:00 2001 From: Erik Date: Wed, 12 Aug 2026 10:26:19 +0200 Subject: [PATCH] =?UTF-8?q?docs:=20FA6=20=E2=80=94=20ledger=20row=20+=20ga?= =?UTF-8?q?te=20script=20=C2=A7FA6=20(fellowship=20PASSED=20live,=20allegi?= =?UTF-8?q?ance=20deferred)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plan ledger: fellowship two-session automated gate PASSED live 2026-08-12 (five of six runs reproduced the decisive cross-session assertion); the allegiance bot gate is DEFERRED behind AllegianceGateEnabled=false pending docs/ISSUES.md #384, with commit citations for every fix this slice landed (confirmation relay, name-matched proximity, the fellowship-only finalization). Gate script §FA6: the fellowship automated-gate recipe + actual PASSED result (the two-session config, the six proof points per stage, the literal decisive-assertion log lines), the allegiance deferral writeup, and a new [TWO-CLIENT] manual step (25) the user's own connected gate can run to help disambiguate #384 (ACE-side rule vs wire-builder defect vs harness-specific drop) using two real graphical clients instead of the testaccount/testaccount2 pair. Co-Authored-By: Claude Fable 5 --- ...26-08-11-fellowship-allegiance-campaign.md | 2 +- .../2026-08-12-campaign-fa-test-script.md | 164 +++++++++++++++++- 2 files changed, 164 insertions(+), 2 deletions(-) diff --git a/docs/plans/2026-08-11-fellowship-allegiance-campaign.md b/docs/plans/2026-08-11-fellowship-allegiance-campaign.md index 8274e57c..dd355ced 100644 --- a/docs/plans/2026-08-11-fellowship-allegiance-campaign.md +++ b/docs/plans/2026-08-11-fellowship-allegiance-campaign.md @@ -312,5 +312,5 @@ before anything builds on them. | FA3 | **FIX-ROUND CLOSED 2026-08-12** — both reviews' MUST-FIX/SHOULD-FIX/NIT findings applied; still owes the user's connected gate (script: `docs/research/2026-08-12-campaign-fa-test-script.md`, itself corrected by this fix round — see MF-1/MF-2 below). | `0a9ca2f1` (`SocialPanelController` + 4 per-page controllers + `SocialPanelRowText`, `UiTemplateListBox.Flush`, `RetailPanelCatalog.SocialPanel`/`WindowNames.SocialPanel`, `RetailUiRuntime` Mount/Tick/F3/F4 wiring, `InteractionRetainedUiComposition`'s `SocialRuntimeBindings`, register row AD-79); `74c3d85d` (fixture generator entry + committed `social_panel_2100006E_1000018F.json`, `SocialPanelLiveMountProbeTests`, `SocialPanelControllerTests`, `RetailPanelCatalogTests` additions, `FixtureLoader` additions); `b6a25110`/`d7e1cffd` (gate script + ledger + research addendum). Fix-round: `9afa05b5` (blast MF-1 scrollbar wiring + blast SF-2/SF-3 rebuild discipline/revision-latch ordering + mechanism SF-1 F3/F4 relabel + mechanism SF-8 disposed guard, code+tests), `35c40a9b` (mechanism SF-2/blast SF-4 allegiance per-frame allocation hoist + mechanism SF-7 coarser-gate FA5 acceptance line), `ae772709` (mechanism SF-4 fellowship checkbox count + mechanism SF-9 row-text doc + blast SF-5 Flush doc + mechanism SF-3 probe assertions), `a5553904` (mechanism MF-1/MF-2 gate-script corrections + mechanism SF-4 gate-script hedge fix + blast N-8 two new gate steps + blast SF-6 #383 timing correction + blast SF-7 bold-marker fix + mechanism SF-1 research-doc U11), this commit (ledger update). | mechanism `docs/research/2026-08-12-fa3-review-mechanism.md` (2 MUST-FIX, 9 SHOULD-FIX, all applied); blast `docs/research/2026-08-12-fa3-review-blast.md` (1 MUST-FIX, 6 SHOULD-FIX, 1 NIT, all applied). **Live-DAT finding (corrects the coordinator addendum, §10):** the real authored `0x2E` tab table pairs button `0x1000028C` ("Allegiance" caption) with page `0x10000291` as the DEFAULT entry — NOT Friends, which the addendum's x-order guess implied; each page's own `P0x57` independently corroborates (Allegiance page `P0x57=0x1000000E` == `ToggleAllegiancePanel`/F3, Fellowship page `P0x57=0x1000000F` == `ToggleFellowshipPanel`/F4). See `SocialPanelController`'s class doc for the full corrected table. **Unrelated fixture drift caught and reverted:** the `ACDREAM_REGENERATE_UI_FIXTURES=1` run used to produce the new fixture also silently regenerated `keyboard_config_21000009.json` and `options_2100002B.json` with large diffs against this machine's currently-installed DAT (pre-existing environment drift, not FA3-caused) — both were `git checkout`'d back to HEAD before committing; only the new fixture is included; the fix round's blast SF-6 correction narrows this drift to exactly those two OP-era fixtures (git timestamps put the drift window at ~18-21h, same day, not "days ago" as originally filed) and adds the mechanism reviewer's independent no-drift confirmation for the new social-panel fixture itself. Friends/Squelch action buttons are honest INERT per D1 (register row AD-79, one row covering both pages' seven controls, not one per button — now cited BY NAME in both page controllers' own doc comments, mechanism SF-6). The fix round's two connected-gate corrections (mechanism MF-1/MF-2) matter most for the still-owed user gate: the script previously carried a REFUTED tab x-order into step 1/9/10 (priming the user to report the correct Allegiance-left-most layout as wrong) and sent the user to a `@allegiance info`-reveals-the-panel trigger that cannot fire post-FA2 (told to report it as a bug when it correctly did nothing) — both are corrected to state the true FA3 expectation. | automated: Release build green throughout; full solution suite green at every commit. **Pre-fix-round baseline 13,233/4/0 (13,237 total) → fix-round 13,238/4/0 (13,242 total), +5 tests, arithmetic exact:** `SocialPanelControllerTests.cs` +5 (`Friends_ScrollbarModel_IsWiredToListBoxScroll`, `Squelch_ScrollbarModel_IsWiredToListBoxScroll`, `Friends_LongRoster_IsReachableViaScrollbar`, `Squelch_LongRoster_IsReachableViaScrollbar`, `Friends_RevisionBumpWhileHidden_DoesNotRebuild_ButRebuildsOnShow`); `SocialPanelLiveMountProbeTests.cs` gained two new assertions inside its existing single env-gated `[Fact]` (net 0 new test count — trivially passes without `ACDREAM_PROBE_LIVE_MOUNT=1`, same as before); two existing `SocialPanelControllerTests.cs` tests were extended in place (`FriendsAndSquelchActionButtons_AreClickable_ButHaveNoHandler` now also covers `0x1000052C`; `Friends_ReactsToRevisionChange_OnTick` now calls `OnShown()` to match the new visibility gate) — net 0 new tests from those two. Directly measured per-project: Cli 4, UI.Abstractions 916, Runtime 1607, Bake 15, Content 130, Headless 119, App 4876/3 skip, Core.Net 895, Core 4676/1 skip — sum 13,238 passed / 4 skipped / 0 failed. | | FA4 | **CODE-CLOSED 2026-08-12, connected gate OWED** — both dual-lens reviews' items applied (5 MUST-FIX + 9 SHOULD-FIX + 4 NIT mechanism; 1 SHOULD-FIX blast), narrow re-review CLOSED with ONE REOPEN (MF-3 0x00A6 placed pre-world), coordinator re-fix `04161def` (latch-advances-only-on-Accepted + RedeclareAfterWorldEntry wired to the post-world EnteredWorld seam; RED-verified regression pins), re-review of the re-fix CLOSED `06dbf1cf` (seam ordering traced: RestoreLayout fires after _inWorld=true + command activation). Final full suite 13,286/4/0. The user's connected gate (several steps `[TWO-CLIENT]`, deferrable to FA6) remains owed. | Original: `357d2032` (Runtime: `IRuntimeFellowshipView.GetMembers`, `SelectionChangeSource.Social`, +4 `RuntimeFellowshipStateTests`); `5bdd0528` (App: `SocialFellowshipPageController` roster/D4/create/actions/checkboxes rewrite, `RowTemplateResolver` extraction, `UiTemplateListBox.FlushPreservingScroll`, `RetailUiRuntime` D6 intercept + `MountSocialPanel` rewiring, Friends/Squelch scrollbar carry-forward 3, `CharacterOptionsPageController` D7 un-dim, extended `SocialPanelLiveMountProbeTests`, +30 tests); `38f08314` (register rows AD-80/AD-81 + AD-78 addendum, gate script §FA4, ledger). **Fix round** (`docs/research/2026-08-12-fa4-review-mechanism.md` + `-blast.md`): `290f9b58` (MUST-FIX 2 — delete `TryAutoRespondToFellowshipInvite`; type-4 dialog test); `5499f058` (MUST-FIX 1 D5 truncation + MUST-FIX 4 world→panel selection sync + SF-1/SF-2/SF-3/N-0/N-1/N-2/N-3, all in `SocialFellowshipPageController.cs`); `df000306` (MUST-FIX 3 panel-level 0x00A6 reconnect re-arm + SF-4 `Dispose` unsubscribe, `SocialPanelController.cs`); `300d8189` (D6/D7/SF-8 dimming reversal — `IgnoreFellowshipRequests`/`FellowshipAutoAcceptRequests`/`FellowshipShareLoot` revert to `StoreOnly`, only `FellowshipShareXP` stays `Live`); `55b17e15` (SF-6 live-mount-probe assertions); `1d743277` (register AD-82/AD-83 + AD-78 count correction, gate-script SF-7/MUST-FIX-1/3/4/2 corrections, plan D7 SF-8 addendum); this commit (ledger). | owed | automated: Release build green throughout; full solution suite green at every commit, both original and fix round. **Original baseline 13,238/4/0 (13,242 total) → FA4-original 13,272/4/0 (13,276 total), +34 tests.** **Fix-round delta: +13 tests, 0 deletions** (the D6 intercept had no dedicated tests to remove) — `SocialFellowshipPageControllerTests.cs` +8 (2 new `[InlineData]` cases on the existing D5 theory for 6/8-fellow truncation, +6 new `[Fact]`: member-leaves/selection-clear, row-build-failure-doesn't-retry, Recruit-reads-membership, world-selects-fellow, world-selects-non-fellow-keeps-selection, optimistic-caption); `SocialPanelControllerTests.cs` +4 (`[Fact]`: D4 panel-level conjunction, reconnect re-arms, reconnect stays silent when not open, Dispose unsubscribes); `GameplayConfirmationControllerTests.cs` +1 (`[Fact]`: type-4 dialog verbatim + accept). `CharacterOptionsPageControllerTests.cs` (dimming set content changed, count 31/19 → 34/16, net 0 new tests) and `SocialPanelLiveMountProbeTests.cs` (assertions added inside its existing env-gated `[Fact]`, net 0 new tests) extended in place. **Final: 13,285 passed / 4 skipped / 0 failed (13,289 total), directly measured** (13,272 + 13 = 13,285; 4 skips unchanged; arithmetic exact). Per-finding disposition: MUST-FIX 1/2/3/4/5 fixed; mechanism SHOULD-FIX 1-9 and NIT N-0/N-1/N-2/N-3 all applied; blast SHOULD-FIX 1 (AD-78 stale count) fixed. Dimmed-row count: 35 (pre-FA4) → 31 (FA4-original, incorrect) → **34 of 50 (fix-round final, correct)** / 16 live — net ONE row (`FellowshipShareXP`) un-dimmed from the pre-FA4 baseline, not four.** `RuntimeFellowshipStateTests.cs` +4 (`GetMembers_*`); `RowTemplateResolverTests.cs` +3; `UiTemplateListBoxFlushPreservingScrollTests.cs` +4; `SocialFellowshipPageControllerTests.cs` +23 (new file — roster build/diff/rebuild, D5 formatting, create-flow gating, member-action wiring, button-enable rules, checkbox wiring, D4 idempotency). `CharacterOptionsPageControllerTests.cs`/`SocialPanelControllerTests.cs`/`SocialPanelLiveMountProbeTests.cs` extended in place (net 0 new tests from those three — one existing assertion's expected counts changed, one probe test gained assertions inside its existing single env-gated `[Fact]`). **Live-DAT verification (`ACDREAM_PROBE_LIVE_MOUNT=1`, real installed DATs, not a fixture):** the fellowship name-entry field builds as `UiField` (Editable=1 confirmed authored), all 11 buttons/checkboxes resolve as `UiButton`, the ListBox's sole template pair (`0x21000030`/`0x10000281`) resolves through the production `RowTemplateResolver` with all 5 checked row fields at the right widget types, every checkbox label/tooltip resolves to real retail English (`ID_PlayerOption_*` in `0x23000003`), the Open/Close captions resolve to `"Open"`/`"Close"` (`ID_Fellowship_*` in `0x23000001`), and a full production-path `SocialFellowshipPageController.Bind()` against the live layout emits zero "not found" console warnings. **Structural finding (not a bug, a design confirmation):** the live dump shows the name field, Create button, and all four checkboxes are children of `0x1000026B` (the NOT-in-fellowship frame) — retail's Create-flow controls are visible ONLY while you have no fellowship, never simultaneously with the roster; the existing empty/full frame-visibility swap already produces this for free (child visibility cascades from an invisible ancestor — `UiElement.cs:486/540/573/586`), so no extra gating code was needed. **Contradictions/deferrals:** (1) the retail `StringInfo` variable-substitution engine (`StringInfo::InqString` → `StringTableMetaLanguage::UnescapeString`) is unresolved in this campaign's decomp scope — row/stats/vitals text renders as numeric composites, not retail's exact sentence (AD-81); (2) `ACCharGenData::FormatName` is unported — the Create flow sends the raw typed name, not retail's canonicalized form (also AD-81); (3) the D5 percentage table's proportional (non-even-split) branch needs a per-level XP-to-next-level table acdream does not have — that branch omits the percentage rather than computing one (documented in `FormatStatsText`'s own doc, not a separate register row); (4) the Recruit button's enable rule does not gate on "target is a player" (retail does) — acdream's UI layer has no cheap classification for this, and the server refuses a non-player target the same way retail's own click-handler silently no-ops, so this is a superset-of-retail enable rule, not a wire-behavior gap. **[FIX-ROUND CORRECTION: item (4)'s "inline comment, not a register row" disposition was itself the wrong call under the register rule (MUST-FIX 5) — filed as register row AD-83 in the fix round.]** | | FA5 | **CODE-CLOSED 2026-08-12, connected gate OWED** — dual-lens review APPROVE-WITH-FIXES (mechanism `f12aefe9`: 1 LOW SHOULD-FIX + nits, **live-mount probe RAN + PASSED against real installed DATs** incl. the scoped doubled-`0x10000492` `NotSame` check — the anti-fixture-green safeguard the implementer couldn't run; blast `b6c4a4fa`: 1 doc-only SHOULD-FIX + 2 nits, all structural axes clean) → coordinator SF-1 fix `eac28dc1` (removed the invented offline-vassal name-grey — retail's cue is the authored `0x100004AA` marker alone; pinned by `Allegiance_OfflineCue_IsTheMarkerOnly_NameStaysWhite`; AD-82 addendum + AD-86 count corrected; the blast doc-only baseline off-by-one fixed in this row below). Full suite **13,297/4/0**. The user's connected gate (several steps `[TWO-CLIENT]`, deferrable to FA6) remains owed. **[Original implementer pass:]** The allegiance page is fully live against FA1's parser and FA2's already-shipped `RuntimeAllegianceState`/`IRuntimeAllegianceCommands` (no Runtime-layer changes needed this slice — FA2 had already built `SetUpdateSubscription`, `Swear`, `Break`, `Kick`, and the full `IRuntimeAllegianceView` accessor set). CF-1's corrected `0x001F` subscription is wired at all three retail arming points (`Bind`'s PostInit attempt, the post-world `EnteredWorld` seam via `RedeclareAfterWorldEntry` — UNCONDITIONAL, explicitly NOT edge-triggered, to avoid repeating FA4's MF-3-REOPEN bug class — and the visible-branch toggle via `SetPageVisible`, folded into `SocialPanelController`'s existing window-shown+active-tab conjunction). Monarch/patron/self blocks implement fix-round SF-7's per-relationship gate (fresh decompile of `UpdatePlayerData`/`UpdateMonarchData`/`UpdatePatronData` pinned every field source: `0x10000251` is the ALLEGIANCE's own name not the viewer's, followers are `TotalVassals`/`TotalMembers-1` straight off the wire, `0x10000492`'s doubled instances are the viewer's own `CpTithed`). The vassal roster reuses FA4's `FlushPreservingScroll` diff pattern in the wire's already-reversed order. Swear/Break/Kick each open a local confirmation dialog before sending (Swear targets the WORLD selection via the same `ClientObjectTable` name resolver `ToolbarRuntimeBindings.ResolveName` uses; Break targets the current patron; Kick targets the panel-local selected vassal row, no world-selection sync, lane A §6.2); the server-driven "accept incoming swear" (`ConfirmationType` 1) needed no new code since `GameplayConfirmationController` already handles every type generically — a new test verifies this explicitly rather than assuming FA4's blast review's claim. Four new register rows: AD-84 (Swear's missing IsPlayer gate, mirrors AD-83), AD-85 (the unported `StringInfo` gap extended to Allegiance's numeric fields + three confirmation dialogs, mirrors AD-81), AD-86 (ACE's seven zeroed profile fields, dropped past acdream's own parse layer to match retail's own no-widget presentation), and an addendum bracket on AD-82 (the vassal-row click-target limitation recurs, but NOT its invented tint colors or its Fellowship-only world-sync). | `7ed79eaf` (code: Runtime command wrapper (`InteractionUiRuntimeSources.cs`), `SocialRuntimeBindings`/`MountSocialPanel` widening (`RetailUiRuntime.cs`), the Allegiance projection delegates (`InteractionRetainedUiComposition.cs`), `SocialPanelController`'s CF-1 wiring, the full `SocialAllegiancePageController.cs` rewrite, plus 10 new `SocialPanelControllerTests.cs` tests, 1 new `GameplayConfirmationControllerTests.cs` type-1 test, and the `SocialPanelLiveMountProbeTests.cs` production-mount extension); this commit (register rows AD-84/AD-85/AD-86 + AD-82 addendum, gate-script §FA5, ledger) | owed | automated: Release build green throughout; full solution suite green. **Baseline 13,286/4/0 (FA4's TRUE close after the MF-3 re-fix `04161def` added the widget test — blast SF-1 corrected the FA5 pass's "13,285/+11" citation, which used FA4's pre-re-fix intermediate figure) → FA5 implementer pass 13,296/4/0, +10 tests net** (`SocialPanelControllerTests.cs` nets +9: the old coarse-gate `Allegiance_HasProfile_ShowsBothBlocks` is REMOVED and replaced by 4 SF-7 per-relationship tests, 1 roster-population test, 3 swear/break/kick wiring tests, and 2 CF-1 subscription-arming tests — 10 added, 1 removed; `GameplayConfirmationControllerTests.cs` +1 the type-1 verification test; `SocialPanelLiveMountProbeTests.cs` extended in place, net 0). **Coordinator SF-1 fix `eac28dc1`: +1 test** (`Allegiance_OfflineCue_IsTheMarkerOnly_NameStaysWhite`; `OfflineNameColor` removed, no test deletion) → **final 13,297/4/0, directly measured.** **Live-DAT verification RAN + PASSED at the mechanism review** (`ACDREAM_PROBE_LIVE_MOUNT=1`, real installed DATs, `Passed:1 Failed:0`): the scoped `0x10000492` dual-resolution `NotSame`, `passupCount==2`, the vassal row template/checkbox/five confirmation-label strings resolving non-empty, and a full production `SocialAllegiancePageController.Bind()` with zero "not found" — the safeguard the implementer's own worktree lacked the DATs to run. **Contradictions/deferrals:** (1) the self-rank field (`0x10000253`) retail-sources from a LIVE buffed-quality query (`CBaseQualities::InqInt(qualities, 0x1e)`, i.e. `PropertyInt.AllegianceRank`) plus a 20-table title lookup (`AllegianceData::GetTitle`) neither of which this controller has a seam for — substitutes the numerically-equivalent `RuntimeAllegianceSnapshot.Rank` from the same `0x0020` message, rendered bare (documented in the class doc, not a separate register row since it is a data-source substitution rather than a presentation gap); (2) "your follower count" (`0x10000252`) was NOT explicitly formula-cited in the panel-structure research doc — a targeted fresh decompile of `UpdatePlayerData` (`pseudo_c:157629`) confirmed `_total_vassals` directly, resolving the ambiguity from primary source rather than inferring it. | `docs/research/2026-08-12-campaign-fa-test-script.md` §FA5 (new, mirrors §FA4's structure: CF-1 subscription steps, the SF-7 per-relationship steps, vassal-list steps, swear/break/kick with their confirmations, `[TWO-CLIENT]` tags deferrable to FA6, ACE-zeroed-field honesty, full "what to report"/"explicitly not in scope" lists) | -| FA6 | — | | | | +| FA6 | **FELLOWSHIP AUTOMATED GATE PASSED LIVE 2026-08-12; ALLEGIANCE BOT GATE DEFERRED** — six live runs against local ACE (`testaccount`/`+Acdream` as Leader, `testaccount2`/`+Horan` as Recruit). The decisive two-session fellowship assertion (the RECRUITED bot's own `RuntimeFellowshipState` — a separate process's canonical Runtime owner, not the Leader's local echo — flipping `IsInFellowship=true`, `MemberCount=2`, `LeaderGuid=0x5000000A`) passed identically in five of the six live runs (1, 3, 4, 5, 6 — every run except run 2, which hit the wrong-target bug below before the fix); the D4 panel-open declaration and recruit-vitals presence (`maxHealth=201`) were confirmed alongside it. Two real live-run findings were fixed in-slice: run 1's fellowship pass then stalled waiting on the allegiance swear, which investigation traced to headless hosts dropping every server-driven confirmation (`OnConfirmationRequest: null`), fixed by wiring a single-slot confirmation-relay latch on `HeadlessSessionHost`; run 2 found a stray third player character on the shared ACE dev instance (`+Je`, `0x50000001`) could be nearer than the actual Recruit bot after `@teleallto`, fixed by `RuntimeFriendlyTargetQuery.FindPlayerByName` (name-matched via `FellowshipAllegianceGateCoordinator`, which carries the Recruit bot's own discovered name — the D8 "discover it live" mechanism) replacing the ambiguous "nearest any player" query. The ALLEGIANCE swear never completes over the wire: ACE returns nothing at all to `Event_SwearAllegiance (0x001D)` — no `0x0274` confirmation, no `0x0020`, no error — confirmed at 0.005 m separation (run6's distance diagnostic), ruling out retail's 2.0 m swear-distance gate. Filed `docs/ISSUES.md` #384 and register row AD-87; `AllegianceGateEnabled` (both policy classes) is `false` by default, keeping every allegiance stage (Leader's `WaitForVassal`; Recruit's `Swear`/`WaitSwornSeed`/`Break`/`WaitBrokenSeed`) written, wired, and ready to re-enable once #384 closes. Per user direction, no further live iteration on the allegiance blocker this slice — deferred to the user's own connected gate (manual swear between two graphical clients). | `6b8e29cd` (`RuntimeFriendlyTargetQuery` + 4 tests), `28255890` (role-discriminated policy + the two-bot gate policy pair), `11641597` (confirmation relay, targeting the LEADER per the wire research's "Target (would-be patron)" step 4 — independently re-verified against `docs/research/2026-08-11-fa-allegiance-wire.md` §1.3 primary source during this slice, which settles that the confirmation belongs on the patron's client, not the swearer's), `ab79b91f` (name-matched proximity + `FindPlayerByName` + 3 tests), `5244e46d` (fellowship-only finalization: `AllegianceGateEnabled` flag, permanent diagnostics), `022b1844` (docs: #384 + AD-87) | none (automated + bot-vs-ACE only, per the plan's own Gate column) | automated + bot-vs-ACE run: `dotnet build -c Release` and the full solution suite green at every commit (final **13,304 passed / 4 skipped / 0 failed**, baseline 13,297/4/0 + 7 new `RuntimeFriendlyTargetQueryTests`). Six live bot-vs-ACE runs against local ACE `127.0.0.1:9000`; every session ended with ACE-confirmed graceful per-character logout (`[session] graceful logout confirmed`) except run 1's very first `idle`-policy probe of `testaccount2`, which an external `timeout` wrapper hard-killed before this slice adopted self-terminating policy runs — the ~3-minute ACE stale-session wait that followed is the only deviation from clean teardown across the whole slice. | | FA7 | — | | | | diff --git a/docs/research/2026-08-12-campaign-fa-test-script.md b/docs/research/2026-08-12-campaign-fa-test-script.md index 6fd4724d..532bbdbd 100644 --- a/docs/research/2026-08-12-campaign-fa-test-script.md +++ b/docs/research/2026-08-12-campaign-fa-test-script.md @@ -4,7 +4,10 @@ with `ACDREAM_LIVE=1` against the local ACE server (`testaccount` / `+Acdream`). Anything marked **INERT** is authored and clickable but deliberately does nothing yet — that is the correct, contracted behavior -for this slice (D1), not a bug. +for this slice (D1), not a bug. **FA6's bot-vs-ACE fellowship gate PASSED +live 2026-08-12 (§FA6 below) and needs no connected re-gate; its +allegiance half is DEFERRED (`docs/ISSUES.md` #384) with one +`[TWO-CLIENT]` manual step in §FA6 that can help settle it.** FA3 is the panel SHELL only: mount, F3/F4 open paths, tab switching, all four pages' empty states, and Friends/Squelch read-only lists. Fellowship @@ -750,3 +753,162 @@ from the steps above, or a pre-existing allegiance) AD-86). - Swear's missing "target is a player" enable-rule check (register row AD-84) — a cosmetic superset-of-retail gap, not a wire-behavior bug. + +--- + +## FA6 — the bot-vs-ACE two-session gate + +Unlike FA3-FA5, this is not a graphical-client gate: no window is +launched. The coordinator runs `acdream-headless run` with a config +declaring TWO sessions — `testaccount`/`+Acdream` as the fellowship +LEADER/allegiance PATRON, `testaccount2` as the fellowship RECRUIT/ +allegiance VASSAL — both selecting policy id `fellowship-allegiance-gate` +with `role: "leader"`/`role: "recruit"`. Both sessions run in the SAME +process, ticked sequentially on one dedicated update thread. + +### Automated fellowship gate — the shipped, passing part + +**Recipe:** + +```json +{ + "version": 1, + "sessions": [ + { + "id": "fa6-bot-a-leader", + "endpoint": { "host": "127.0.0.1", "port": 9000 }, + "account": "testaccount", + "character": { "name": "+Acdream" }, + "policy": { "id": "fellowship-allegiance-gate", "role": "leader" }, + "credential": { "provider": "environment", "reference": "FA6_BOT_A_PASSWORD" } + }, + { + "id": "fa6-bot-b-recruit", + "endpoint": { "host": "127.0.0.1", "port": 9000 }, + "account": "testaccount2", + "character": { "index": 0 }, + "policy": { "id": "fellowship-allegiance-gate", "role": "recruit" }, + "credential": { "provider": "environment", "reference": "FA6_BOT_B_PASSWORD" }, + "characterOptions": { "FellowshipAutoAcceptRequests": true } + } + ] +} +``` + +The Recruit session's `character` selector deliberately does NOT hard-code +a name — `{"index": 0}` picks the first available character on +`testaccount2`, and the Leader bot discovers that character's real name +live (`FellowshipAllegianceGateCoordinator.RecruitCharacterName`, set by +the Recruit bot's own `HeadlessSessionHost` the instant CharacterList +selection resolves it) rather than trusting a config-file guess. Run it: +`acdream-headless run --config fa6-gate.json` with `FA6_BOT_A_PASSWORD`/ +`FA6_BOT_B_PASSWORD` set in the environment. The process is +self-terminating — every policy stage carries its own timeout, and the +run ends (gracefully, with ACE-confirmed per-character logout) on its own +once both bots reach `Done` or fault. Do not `timeout`/Ctrl+C the process +mid-run: an external kill during an in-world session leaves ACE holding a +stale session for ~3 minutes (CLAUDE.md's documented logout-before- +reconnect discipline; one of this slice's OWN runs discovered this +firsthand from wrapping a probe run in bash's `timeout`, not from the +gate's own code path). + +**What the fellowship gate proves, per stage:** + +1. **Proximity.** The Leader sends retail's admin `@teleallto` (no target + = "teleport everyone online to me") on a 5 s retry, then polls + `RuntimeFriendlyTargetQuery.FindPlayerByName` for the Recruit bot's + discovered name specifically — NOT the ambiguous "nearest any player" + query, which a live run found picks up a stray third character on a + shared ACE dev instance if one happens to be online nearby. +2. **Create + recruit.** `Fellowship.Create` then `Fellowship.Recruit` + against the name-matched guid. +3. **The decisive assertion.** The Recruit bot's OWN + `RuntimeFellowshipState` — a completely separate process's canonical + Runtime owner, not the Leader's local echo — flips + `IsInFellowship=true`, `MemberCount=2`, `LeaderGuid=`. + This is the proof that `0x02BE`/`0x02C0` inbound reached a real second + client, not just that the Leader's own send succeeded. +4. **D4 panel-open + vitals.** The Leader declares `SetPanelOpen(true)` + (`0x00A6`) and asserts the Recruit's own fellowship member row already + carries nonzero vitals (`MaxHealth`) — headless bots have no panel to + freeze/unfreeze, so this proves the declaration sends and that vitals + data is present, not the freeze-while-closed half of D4 (that remains + a graphical-client-only observation, FA4's own owed connected gate). +5. **Mid-flow reconnect.** Both bots reconnect + (`commands.Session.Reconnect`) after establishing, then assert the + fellowship state re-seeds from scratch (a fresh generation clears + `RuntimeFellowshipState` per FA2 D2) — proving reconnect-idempotence + over the real wire, with a genuine second account watching. +6. **Teardown.** The Leader disbands (`Fellowship.Quit(disband: true)`); + both bots assert their own `IsInFellowship` clears. + +**Result — PASSED, 2026-08-12 (five of six live runs; the sixth, run 2, +hit a since-fixed wrong-target bug before the decisive stage was even +reached):** + +``` +[fa6-recruit] DECISIVE ASSERTION PASSED: own RuntimeFellowshipState +flipped IsInFellowship=true, MemberCount=2, LeaderGuid=0x5000000A — +recruit inbound path reached THIS process's own Runtime owner +[fa6-leader] panel-open declared; recruit vitals name='+Horan' +maxHealth=201 +``` + +Two real defects surfaced and were fixed IN this slice, both now covered +by conformance tests: headless hosts silently dropped every server-driven +confirmation (`HeadlessSessionHost.OnConfirmationRequest` was `null`); and +`RuntimeFriendlyTargetQuery.FindClosestOtherPlayer`'s "nearest any player" +query picked up a stray third character (`+Je`, `0x50000001`) instead of +the actual Recruit bot after `@teleallto`, fixed by the name-matched +`FindPlayerByName`. + +### Allegiance bot gate — DEFERRED (docs/ISSUES.md #384) + +**Status: not shipped as an automated gate.** The allegiance half of the +SAME policy classes (Leader's `WaitForVassal`; Recruit's `Swear`/ +`WaitSwornSeed`/`Break`/`WaitBrokenSeed`) is fully written and wired — +proximity, the `0x001D` swear send, a confirmation-relay seam on the +Leader (the would-be patron, per `docs/research/2026-08-11-fa-allegiance- +wire.md` §1.3's "Target (would-be patron)" steps 4-7), the decisive +`TotalVassals`/`TryGetPatron` assertions, break, and reconnect-idempotence +— but is gated OFF by default (`AllegianceGateEnabled = false` in both +policy classes) because it has never actually completed over the wire. +Six live runs against local ACE all showed the same symptom: ACE returns +**nothing** to the swear — no `0x0274` confirmation, no `0x0020` tree +update, no WeenieError — confirmed at 0.005 m separation between the two +bots (ruling out retail's 2.0 m swear-distance gate). See `docs/ISSUES.md` +#384 for the full evidence trail and register row AD-87 for the +divergence this creates (the allegiance wire/state code is unit-tested +and locally-echoed but not wire-verified end-to-end). + +**[TWO-CLIENT] Manual counterpart for the user's connected gate** — this +is the step that can settle #384 outside the headless harness: + +25. With TWO graphical clients logged into different characters near each + other (the existing FA5 `[TWO-CLIENT]` swear steps 14-16 already + exercise this UI path), have character A select character B and click + Swear. **Report specifically:** does B's client show the incoming + "accept swear" confirmation dialog at all? If B accepts, does A's + Allegiance page then show B in A's vassal list, and does B's page show + A as patron? If the SAME silent non-response reproduces in the + graphical client (no dialog ever appears on B's side, no error on + A's), that points at an ACE-side rule or a genuine defect in acdream's + `0x001D` wire builder rather than anything specific to the headless + harness — the two clients are real user accounts, not `testaccount`/ + `testaccount2`, which also rules out any GM-flag-specific explanation. + If it works normally in the graphical client, the symptom is scoped + to `testaccount`/`testaccount2` specifically (a GM flag, a rank/self- + allegiance precondition, or state left over from this session's + testing) — re-run the headless gate with a different account pair + before reopening #384 as a real wire bug. + +### Explicitly NOT in scope for this gate + +- A fully verified allegiance bot gate — deferred to #384; do not report + its absence as a regression, and do not re-run the headless allegiance + path repeatedly hoping for a different result without new information + (six identical live results is enough evidence, not bad luck). +- Kick — no test exercises it (would need a THIRD account to have a + vassal to kick that isn't also the tester). +- Kick/kick-confirmation UI — FA5's own `[TWO-CLIENT]` scope, unrelated + to this bot gate.