fix(ui): FA4 re-review REOPEN — re-declare 0x00A6 from the post-world seam, not the pre-world reset
The FA4 fix round's MUST-FIX 3 placed the 0x00A6 reconnect re-arm at the
wrong lifecycle point (re-review 8bbceff5): ResetSessionTransientUi runs
via the SessionDialogs reset stage BEFORE _inWorld=true, so SetPanelOpen
(world-gated, Validate requireWorld:true) returned Inactive and published
nothing — yet _pageVisible was latched true anyway, so no later hook
re-declared and fellow vitals stayed frozen for the whole new session.
The unit test passed only because the fake recorded unconditionally.
Two-part fix, both retail-faithful mechanisms not suppressions:
- SocialFellowshipPageController.SetPageVisible advances the edge-trigger
latch ONLY when the declaration is Accepted (published), so a dropped
pre-world send leaves the latch clear and a later attempt retries.
- ResetSessionDeclaration (pre-world) now ONLY clears the latch; the new
RedeclareAfterWorldEntry fires from the LiveSession EnteredWorld seam
(wired via RestoreLayout, idempotent if a persisted layout already
re-showed the page) so a still-open Fellowship page re-declares 0x00A6
in world and vitals resume.
Regression pins that actually catch it (the prior test could not):
- SetPageVisible_DoesNotLatch_WhenDeclarationDropped_SoItRetriesInWorld
(widget-level root, world-gated fake);
- Reconnect_ReDeclares0x00A6_AfterWorldEntry_NotDuringPreWorldReset +
Reconnect_StaysSilent_WhenFellowshipPageIsNotActuallyOpen (panel-level,
world-gated). RED-verified: reintroducing the pre-world declaration
fails the reconnect test.
Full Release suite: 13,286 passed / 4 skipped / 0 failed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
8bbceff594
commit
04161defd8
7 changed files with 148 additions and 42 deletions
|
|
@ -116,6 +116,7 @@ public sealed class SocialFellowshipPageControllerTests
|
|||
}
|
||||
|
||||
private static readonly RuntimeCommandResult InactiveResult = new(RuntimeCommandStatus.Inactive, default);
|
||||
private static readonly RuntimeCommandResult AcceptedResult = new(RuntimeCommandStatus.Accepted, default);
|
||||
|
||||
private sealed class FellowshipBindingsBuilder
|
||||
{
|
||||
|
|
@ -127,6 +128,11 @@ public sealed class SocialFellowshipPageControllerTests
|
|||
public uint LocalPlayerGuid;
|
||||
public Func<uint, uint, UiElement?> TemplateResolver = FakeRowResolver;
|
||||
public Func<uint, uint, string?> ResolveString = static (_, _) => null;
|
||||
// SetPanelOpen (0x00A6) is the one command whose CONTROLLER behavior
|
||||
// depends on the result status (MUST-FIX 3 re-fix: the latch advances
|
||||
// only on Accepted). Default in-world so the edge-trigger tests model
|
||||
// production; a test flips this false to simulate a pre-world reset.
|
||||
public bool PanelOpenInWorld = true;
|
||||
|
||||
public SocialFellowshipPageController.Bindings Build() => new(
|
||||
Snapshot: () => Snapshot,
|
||||
|
|
@ -138,7 +144,7 @@ public sealed class SocialFellowshipPageControllerTests
|
|||
Quit: disband => { Calls.Add($"quit:{disband}"); return InactiveResult; },
|
||||
AssignLeader: guid => { Calls.Add($"assign-leader:{guid:X8}"); return InactiveResult; },
|
||||
SetOpen: isOpen => { Calls.Add($"set-open:{isOpen}"); return InactiveResult; },
|
||||
SetPanelOpen: panelOpen => { Calls.Add($"set-panel-open:{panelOpen}"); return InactiveResult; },
|
||||
SetPanelOpen: panelOpen => { Calls.Add($"set-panel-open:{panelOpen}"); return PanelOpenInWorld ? AcceptedResult : InactiveResult; },
|
||||
Selection: Selection,
|
||||
LocalPlayerGuid: () => LocalPlayerGuid,
|
||||
CurrentCharacterOption: id => Options.TryGetValue(id, out bool v) && v,
|
||||
|
|
@ -758,4 +764,32 @@ public sealed class SocialFellowshipPageControllerTests
|
|||
controller.SetPageVisible(false);
|
||||
Assert.Contains("set-panel-open:False", b.Calls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetPageVisible_DoesNotLatch_WhenDeclarationDropped_SoItRetriesInWorld()
|
||||
{
|
||||
// MUST-FIX 3 re-fix (FA4 re-review REOPEN): a declaration attempted
|
||||
// before world entry is dropped (Inactive). The latch must NOT advance
|
||||
// on a dropped publish — otherwise the in-world retry is deduplicated
|
||||
// away and the server never learns the panel is open (fellow vitals
|
||||
// freeze). This is the widget-level root of the reconnect bug.
|
||||
UiElement root = BuildPageRoot(out _, out _);
|
||||
var b = new FellowshipBindingsBuilder
|
||||
{
|
||||
Snapshot = new RuntimeFellowshipSnapshot { IsInFellowship = true },
|
||||
PanelOpenInWorld = false, // pre-world: SetPanelOpen returns Inactive
|
||||
};
|
||||
SocialFellowshipPageController controller = SocialFellowshipPageController.Bind(root, b.Build())!;
|
||||
b.Calls.Clear();
|
||||
|
||||
controller.SetPageVisible(true);
|
||||
Assert.Contains("set-panel-open:True", b.Calls); // attempted, dropped
|
||||
b.Calls.Clear();
|
||||
|
||||
// In world now: the SAME visible=true re-attempts (not deduplicated,
|
||||
// because the dropped attempt never latched) and this time it sticks.
|
||||
b.PanelOpenInWorld = true;
|
||||
controller.SetPageVisible(true);
|
||||
Assert.Contains("set-panel-open:True", b.Calls);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ public sealed class SocialPanelControllerTests
|
|||
{
|
||||
private static readonly RuntimeCommandResult InactiveResult =
|
||||
new(RuntimeCommandStatus.Inactive, default);
|
||||
private static readonly RuntimeCommandResult AcceptedResult =
|
||||
new(RuntimeCommandStatus.Accepted, default);
|
||||
|
||||
/// <summary>Campaign FA slice FA4: the Fellowship page's full read/write
|
||||
/// seam. Every command records its call (by name) into
|
||||
|
|
@ -35,9 +37,14 @@ public sealed class SocialPanelControllerTests
|
|||
uint localPlayerGuid = 0u,
|
||||
Func<CharacterOptionId, bool>? currentCharacterOption = null,
|
||||
Func<uint, uint, UiElement?>? templateResolver = null,
|
||||
Func<uint, uint, string?>? resolveString = null)
|
||||
Func<uint, uint, string?>? resolveString = null,
|
||||
Func<bool>? panelOpenInWorld = null)
|
||||
{
|
||||
calls ??= new List<string>();
|
||||
// SetPanelOpen (0x00A6) is world-gated in production; only its result
|
||||
// status feeds the MUST-FIX 3 latch. Default in-world (Accepted) so
|
||||
// the D4-conjunction tests model production; a reconnect test flips it.
|
||||
Func<bool> inWorld = panelOpenInWorld ?? (static () => true);
|
||||
return new SocialFellowshipPageController.Bindings(
|
||||
Snapshot: () => snapshot,
|
||||
Members: () => members ?? [],
|
||||
|
|
@ -48,7 +55,7 @@ public sealed class SocialPanelControllerTests
|
|||
Quit: disband => { calls.Add($"fellowship-quit:{disband}"); return InactiveResult; },
|
||||
AssignLeader: guid => { calls.Add($"fellowship-assign-leader:{guid:X8}"); return InactiveResult; },
|
||||
SetOpen: isOpen => { calls.Add($"fellowship-set-open:{isOpen}"); return InactiveResult; },
|
||||
SetPanelOpen: panelOpen => { calls.Add($"fellowship-set-panel-open:{panelOpen}"); return InactiveResult; },
|
||||
SetPanelOpen: panelOpen => { calls.Add($"fellowship-set-panel-open:{panelOpen}"); return inWorld() ? AcceptedResult : InactiveResult; },
|
||||
Selection: selection ?? new SelectionState(),
|
||||
LocalPlayerGuid: () => localPlayerGuid,
|
||||
CurrentCharacterOption: currentCharacterOption ?? (_ => false),
|
||||
|
|
@ -61,12 +68,13 @@ public sealed class SocialPanelControllerTests
|
|||
RuntimeFellowshipSnapshot fellowship = default,
|
||||
RuntimeAllegianceSnapshot allegiance = default,
|
||||
FriendsState? friends = null,
|
||||
SquelchState? squelch = null)
|
||||
SquelchState? squelch = null,
|
||||
Func<bool>? panelOpenInWorld = null)
|
||||
{
|
||||
calls ??= new List<string>();
|
||||
return new SocialPanelController.Callbacks(
|
||||
Toggle: () => calls.Add("toggle"),
|
||||
Fellowship: MakeFellowshipBindings(calls, fellowship),
|
||||
Fellowship: MakeFellowshipBindings(calls, fellowship, panelOpenInWorld: panelOpenInWorld),
|
||||
AllegianceSnapshot: () => allegiance,
|
||||
Friends: friends ?? new FriendsState(),
|
||||
Squelch: squelch ?? new SquelchState(),
|
||||
|
|
@ -538,49 +546,73 @@ public sealed class SocialPanelControllerTests
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// MUST-FIX 3 (FA4 fix round) — a generation reset (reconnect) while
|
||||
/// the Fellowship page is still open must re-declare <c>0x00A6</c> on
|
||||
/// the fresh session, not silently stay latched from the old one.
|
||||
/// MUST-FIX 3 (FA4 fix round) + its re-review REOPEN re-fix: a reconnect
|
||||
/// while the Fellowship page stays open must re-declare <c>0x00A6</c> on
|
||||
/// the fresh session — but ONLY AFTER world entry. The generation reset
|
||||
/// (<see cref="SocialPanelController.ResetSessionDeclaration"/>) runs
|
||||
/// BEFORE the new session is in world, where <c>SetPanelOpen</c> is
|
||||
/// dropped (Inactive); it must only clear the latch. The re-declaration
|
||||
/// is <see cref="SocialPanelController.RedeclareAfterWorldEntry"/>, wired
|
||||
/// to the post-world <c>EnteredWorld</c> seam. The original fix declared
|
||||
/// from the pre-world reset — the fake recorded a send the real
|
||||
/// world-gated command would have dropped, so the roster stayed frozen.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ResetSessionDeclaration_ReArms0x00A6_ForAStillOpenFellowshipPage()
|
||||
public void Reconnect_ReDeclares0x00A6_AfterWorldEntry_NotDuringPreWorldReset()
|
||||
{
|
||||
var calls = new List<string>();
|
||||
bool inWorld = true;
|
||||
ImportedLayout layout = FixtureLoader.LoadSocialPanelHost();
|
||||
SocialPanelController? controller = SocialPanelController.Bind(
|
||||
layout,
|
||||
MakeCallbacks(calls, fellowship: new RuntimeFellowshipSnapshot { IsInFellowship = true }));
|
||||
MakeCallbacks(
|
||||
calls,
|
||||
fellowship: new RuntimeFellowshipSnapshot { IsInFellowship = true },
|
||||
panelOpenInWorld: () => inWorld));
|
||||
Assert.NotNull(controller);
|
||||
controller!.ActivateTabs();
|
||||
controller.OnShown();
|
||||
controller.ShowFellowship();
|
||||
Assert.Contains("fellowship-set-panel-open:True", calls);
|
||||
Assert.Contains("fellowship-set-panel-open:True", calls); // declared in world
|
||||
calls.Clear();
|
||||
|
||||
// Simulate a reconnect: generation reset while the panel stays open
|
||||
// on the Fellowship tab (nothing else in this controller changes).
|
||||
// Reconnect: the generation reset runs BEFORE the new session is in
|
||||
// world. The pre-world reset must leave NO published 0x00A6 (the
|
||||
// REOPEN bug latched a dropped send here and never retried).
|
||||
inWorld = false;
|
||||
controller.ResetSessionDeclaration();
|
||||
Assert.DoesNotContain(calls, c => c.StartsWith("fellowship-set-panel-open"));
|
||||
|
||||
// World entry: the post-world seam re-declares, now Accepted.
|
||||
inWorld = true;
|
||||
controller.RedeclareAfterWorldEntry();
|
||||
Assert.Contains("fellowship-set-panel-open:True", calls);
|
||||
}
|
||||
|
||||
/// <summary>MUST-FIX 3's counterpart: a reconnect while the panel is
|
||||
/// closed (or on a different tab) must NOT spuriously declare
|
||||
/// <c>0x00A6</c> true.</summary>
|
||||
/// closed (or on a different tab) must NOT declare <c>0x00A6</c> true,
|
||||
/// even after world entry.</summary>
|
||||
[Fact]
|
||||
public void ResetSessionDeclaration_StaysSilent_WhenFellowshipPageIsNotActuallyOpen()
|
||||
public void Reconnect_StaysSilent_WhenFellowshipPageIsNotActuallyOpen()
|
||||
{
|
||||
var calls = new List<string>();
|
||||
bool inWorld = true;
|
||||
ImportedLayout layout = FixtureLoader.LoadSocialPanelHost();
|
||||
SocialPanelController? controller = SocialPanelController.Bind(
|
||||
layout,
|
||||
MakeCallbacks(calls, fellowship: new RuntimeFellowshipSnapshot { IsInFellowship = true }));
|
||||
MakeCallbacks(
|
||||
calls,
|
||||
fellowship: new RuntimeFellowshipSnapshot { IsInFellowship = true },
|
||||
panelOpenInWorld: () => inWorld));
|
||||
Assert.NotNull(controller);
|
||||
controller!.ActivateTabs(); // default tab: Allegiance
|
||||
controller!.ActivateTabs(); // default tab: Allegiance (Fellowship not active)
|
||||
controller.OnShown();
|
||||
calls.Clear();
|
||||
|
||||
inWorld = false;
|
||||
controller.ResetSessionDeclaration();
|
||||
inWorld = true;
|
||||
controller.RedeclareAfterWorldEntry();
|
||||
|
||||
Assert.DoesNotContain(calls, c => c.StartsWith("fellowship-set-panel-open"));
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue