using AcDream.Core.Net.Messages; using AcDream.Runtime.Gameplay; namespace AcDream.Runtime.Tests.Gameplay; /// /// Campaign FA slice FA2 (2026-08-12): lifecycle rules for /// — seeding from the unsolicited /// AllegianceUpdate push and from a self-gated /// AllegianceInfoResponse, the HasServerSeed-style latch, the /// monarch/patron/vassal walk, revision monotonicity, and the "survives /// reconnect" ownership contract (no RuntimeGenerationReset stage — /// see for the sibling assertion /// that Fellowship IS a reset stage and Allegiance is not). /// public sealed class RuntimeAllegianceStateTests { private const uint MonarchGuid = 0x50000001u; private const uint PatronGuid = 0x50000002u; private const uint SelfGuid = 0x50000003u; private const uint VassalAGuid = 0x50000004u; private const uint VassalBGuid = 0x50000005u; private static ClientCommandResponses.AllegianceMemberRecord Record( uint id, uint parent, string name, bool loggedIn = true) => new(id, parent, loggedIn, name); private static ClientCommandResponses.AllegianceUpdate Update(uint rank = 3u) => new( rank, TotalMembers: 5u, TotalVassals: 3u, RecordCount: 5, AllegianceName: "The Order", Monarch: Record(MonarchGuid, 0u, "Monarch"), Records: [ Record(PatronGuid, MonarchGuid, "Patron"), Record(SelfGuid, PatronGuid, "Self"), Record(VassalAGuid, SelfGuid, "VassalA"), Record(VassalBGuid, SelfGuid, "VassalB"), ]); [Fact] public void ApplyUpdate_SeedsTheProfileAndArmsHasServerSeed() { var state = new RuntimeAllegianceState(); Assert.False(state.HasServerSeed); state.ApplyUpdate(Update(rank: 7u)); RuntimeAllegianceSnapshot snapshot = state.View.Snapshot; Assert.True(state.HasServerSeed); Assert.True(snapshot.HasServerSeed); Assert.True(snapshot.HasProfile); Assert.Equal(7u, snapshot.Rank); Assert.Equal("The Order", snapshot.AllegianceName); Assert.Equal(MonarchGuid, snapshot.MonarchGuid); Assert.True(snapshot.HasMonarch); Assert.Equal(4, snapshot.RecordCount); } [Fact] public void ApplyInfoResponseSelf_SeedsTheProfileButLeavesRankUntouched() { var state = new RuntimeAllegianceState(); state.ApplyUpdate(Update(rank: 9u)); var response = new ClientCommandResponses.AllegianceInfoResponse( TargetGuid: SelfGuid, TotalMembers: 5u, TotalVassals: 3u, RecordCount: 5, AllegianceName: "The Order", Monarch: Record(MonarchGuid, 0u, "Monarch"), Records: [ Record(PatronGuid, MonarchGuid, "Patron"), Record(SelfGuid, PatronGuid, "Self"), ]); state.ApplyInfoResponseSelf(response); RuntimeAllegianceSnapshot snapshot = state.View.Snapshot; Assert.True(snapshot.HasProfile); // AllegianceInfoResponse carries no rank field on the wire — the // last known rank from the earlier AllegianceUpdate is retained. Assert.Equal(9u, snapshot.Rank); Assert.Equal(2, snapshot.RecordCount); } [Fact] public void TryGetMonarchPatronAndVassals_WalkTheFlatRecordList() { var state = new RuntimeAllegianceState(); state.ApplyUpdate(Update()); Assert.True(state.View.TryGetMonarch(out RuntimeAllegianceMemberSnapshot monarch)); Assert.Equal("Monarch", monarch.Name); Assert.True(state.View.TryGetPatron(SelfGuid, out RuntimeAllegianceMemberSnapshot patron)); Assert.Equal(PatronGuid, patron.CharacterId); // The monarch has no patron. Assert.False(state.View.TryGetPatron(MonarchGuid, out _)); var vassals = state.View.GetVassals(SelfGuid).ToList(); Assert.Equal(2, vassals.Count); Assert.Contains(vassals, v => v.CharacterId == VassalAGuid); Assert.Contains(vassals, v => v.CharacterId == VassalBGuid); Assert.True(state.View.TryGetMember(VassalAGuid, out RuntimeAllegianceMemberSnapshot vassalA)); Assert.Equal("VassalA", vassalA.Name); Assert.False(state.View.TryGetMember(0x99999999u, out _)); } [Fact] public void GetVassals_VisitsSiblingsInReverseWireOrder() { // Lane C §4.4 point 3: each new record is PREPENDED to its parent's // vassal list on assembly — the record parsed LAST under a given // parent renders FIRST. var state = new RuntimeAllegianceState(); state.ApplyUpdate(Update()); var vassals = state.View.GetVassals(SelfGuid).ToList(); Assert.Equal(VassalBGuid, vassals[0].CharacterId); Assert.Equal(VassalAGuid, vassals[1].CharacterId); } [Fact] public void Revision_IsMonotonicAcrossEveryEventKind() { var state = new RuntimeAllegianceState(); long r0 = state.View.Snapshot.Revision; state.ApplyUpdate(Update()); long r1 = state.View.Snapshot.Revision; Assert.True(r1 > r0); state.ApplyLoginNotification(VassalAGuid, isLoggedIn: true); long r2 = state.View.Snapshot.Revision; Assert.True(r2 > r1); state.ApplyUpdateDone(weenieError: 0u); long r3 = state.View.Snapshot.Revision; Assert.True(r3 > r2); state.ApplyUpdateAborted(weenieError: 0u); long r4 = state.View.Snapshot.Revision; Assert.True(r4 > r3); } [Fact] public void CaptureOwnership_ConvergesOnlyAfterDispose() { var state = new RuntimeAllegianceState(); state.ApplyUpdate(Update()); Assert.False(state.CaptureOwnership().IsConverged); state.Dispose(); RuntimeAllegianceOwnershipSnapshot retired = state.CaptureOwnership(); Assert.True(retired.IsConverged); Assert.True(retired.IsDisposed); Assert.False(retired.HasProfile); Assert.Equal(0, retired.RecordCount); Assert.False(state.HasServerSeed); } [Fact] public void MutatingAfterDispose_Throws() { var state = new RuntimeAllegianceState(); state.Dispose(); Assert.Throws(() => state.ApplyUpdate(Update())); Assert.Throws( () => state.ApplyLoginNotification(SelfGuid, true)); } }