acdream/tests/AcDream.App.Tests/UI/GameplayConfirmationControllerTests.cs
Erik 290f9b584a fix #FA4-mechanism-MUST-FIX-2: delete the non-retail client-side fellowship-invite intercept
RetailUiRuntime.TryAutoRespondToFellowshipInvite auto-declined/auto-accepted
fellowship invites based on IgnoreFellowshipRequests/FellowshipAutoAcceptRequests
before the dialog ever reached GameplayConfirmationController. Byte-verified
across Handle_Character__ConfirmationRequest @0x005640A0,
RecvNotice_FellowshipRequest @0x00490880, and MakeFellowRequestDialog
@0x00490620 (whose only guard is m_fellowRequestContext) plus a whole-file
sweep of both option accessors: retail's client reads neither bit on any
confirmation path. ACE filters both bits server-side, so the interceptor was
dead code against a correct ACE and actively harmful against a drifting one
(IgnoreFellowshipRequests defaults true, so it would silently swallow real
invites with no dialog and no chat line).

HandleConfirmationRequest now routes every confirmation type, including 4,
straight to the generic controller -- exactly like retail. No tests existed
for the deleted interceptor (nothing to remove); added a test proving the
type-4 dialog renders the server message verbatim (not "Continue?"-suffixed)
and sends accept/decline through the generic path.

Per the corrected plan D6 (docs/plans/2026-08-11-fellowship-allegiance-campaign.md).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 07:32:41 +02:00

118 lines
5.1 KiB
C#

using AcDream.App.UI;
using AcDream.App.UI.Layout;
using AcDream.App.Tests.UI.Layout;
using AcDream.Core.Net.Messages;
namespace AcDream.App.Tests.UI;
public sealed class GameplayConfirmationControllerTests
{
[Fact]
public void SkillRequestAppendsContinueAndCloseNoticeSendsServerTuple()
{
var root = new UiRoot { Width = 800f, Height = 600f };
ImportedLayout? shown = null;
var factory = new RetailDialogFactory(root, _ =>
shown = FixtureLoader.LoadConfirmationDialog());
var responses = new List<(uint Type, uint Context, bool Accepted)>();
using var controller = new GameplayConfirmationController(
factory,
(type, context, accepted) => responses.Add((type, context, accepted)));
Assert.True(controller.HandleRequest(
new GameEvents.CharacterConfirmationRequest(2u, 42u, "Raise this skill?")));
Assert.Equal(
"Raise this skill? Continue?",
string.Join(" ", Assert.IsType<UiText>(shown!.FindElement(
RetailConfirmationDialogView.MessageElementId)).LinesProvider().Select(static line => line.Text)));
Assert.IsType<UiButton>(shown.FindElement(
RetailConfirmationDialogView.AcceptButtonId)).OnClick!();
Assert.Equal([(2u, 42u, true)], responses);
Assert.Equal(0u, controller.ActiveDialogContext);
}
/// <summary>
/// MUST-FIX 2 (FA4 fix round, 2026-08-12): a fellowship invite
/// (<c>ConfirmationType.Fellowship</c>, type 4) reaches this generic
/// controller and opens a dialog exactly like any other type — no
/// client-side interceptor exists anymore (D6's correction: retail's
/// client reads neither option bit on the invite path;
/// <c>RetailUiRuntime.HandleConfirmationRequest</c> now routes every
/// type, including 4, straight here). Type 4 is NOT in the
/// 2/3/5/6 " Continue?"-suffix set, so the message renders verbatim —
/// matching <c>Handle_Character__ConfirmationRequest @0x005640A0</c>'s
/// case-4 arm, which is a single call with no text transformation.
/// </summary>
[Fact]
public void FellowshipInviteRequest_Type4_OpensDialog_MessageVerbatim_AndSendsAcceptOnClose()
{
var root = new UiRoot { Width = 800f, Height = 600f };
ImportedLayout? shown = null;
var factory = new RetailDialogFactory(root, _ =>
shown = FixtureLoader.LoadConfirmationDialog());
var responses = new List<(uint Type, uint Context, bool Accepted)>();
using var controller = new GameplayConfirmationController(
factory,
(type, context, accepted) => responses.Add((type, context, accepted)));
Assert.True(controller.HandleRequest(
new GameEvents.CharacterConfirmationRequest(4u, 7u, "Alice invites you to join their fellowship.")));
Assert.Equal(
"Alice invites you to join their fellowship.",
string.Join(" ", Assert.IsType<UiText>(shown!.FindElement(
RetailConfirmationDialogView.MessageElementId)).LinesProvider().Select(static line => line.Text)));
Assert.IsType<UiButton>(shown.FindElement(
RetailConfirmationDialogView.AcceptButtonId)).OnClick!();
Assert.Equal([(4u, 7u, true)], responses);
Assert.Equal(0u, controller.ActiveDialogContext);
}
[Fact]
public void MatchingConfirmationDoneClosesDialogAndUnmatchedTupleDoesNothing()
{
var root = new UiRoot { Width = 800f, Height = 600f };
var factory = new RetailDialogFactory(root, _ =>
FixtureLoader.LoadConfirmationDialog());
var responses = new List<(uint Type, uint Context, bool Accepted)>();
using var controller = new GameplayConfirmationController(
factory,
(type, context, accepted) => responses.Add((type, context, accepted)));
controller.HandleRequest(new GameEvents.CharacterConfirmationRequest(7u, 99u, "Proceed?"));
Assert.False(controller.HandleDone(
new GameEvents.CharacterConfirmationDone(7u, 100u)));
Assert.True(factory.IsOpen);
Assert.True(controller.HandleDone(
new GameEvents.CharacterConfirmationDone(7u, 99u)));
Assert.False(factory.IsOpen);
Assert.Equal([(7u, 99u, false)], responses);
}
[Fact]
public void FactoryReset_CompletesResponseBeforeSessionTupleIsForgotten()
{
var root = new UiRoot { Width = 800f, Height = 600f };
var factory = new RetailDialogFactory(root, _ =>
FixtureLoader.LoadConfirmationDialog());
var responses = new List<(uint Type, uint Context, bool Accepted)>();
using var controller = new GameplayConfirmationController(
factory,
(type, context, accepted) => responses.Add((type, context, accepted)));
controller.HandleRequest(
new GameEvents.CharacterConfirmationRequest(7u, 99u, "Proceed?"));
factory.Reset();
controller.ResetSession();
Assert.Equal(0u, controller.ActiveDialogContext);
Assert.Equal([(7u, 99u, false)], responses);
Assert.False(factory.IsOpen);
}
}