diff --git a/SVSim.UnitTests/BattleNode/Integration/BattleNodeFlowTests.cs b/SVSim.UnitTests/BattleNode/Integration/BattleNodeFlowTests.cs index 4f0d88c..acababe 100644 --- a/SVSim.UnitTests/BattleNode/Integration/BattleNodeFlowTests.cs +++ b/SVSim.UnitTests/BattleNode/Integration/BattleNodeFlowTests.cs @@ -218,8 +218,7 @@ public class BattleNodeFlowTests await using var _b = clientB; await Task.WhenAll(clientA.ConsumeHandshakeAsync(ct), clientB.ConsumeHandshakeAsync(ct)); - await DriveHandshakeAsync(clientA, vidA, key, ct); - await DriveHandshakeAsync(clientB, vidB, key, ct); + await DrivePvpHandshakeAsync(clientA, vidA, clientB, vidB, key, ct); // Both are now AfterReady. A sends TurnEnd; both should receive TurnEnd + Judge. await clientA.SendMsgAsync(MakeEnvelopeWith(vidA, NetworkBattleUri.TurnEnd, pubSeq: 5), key, ct); @@ -268,8 +267,7 @@ public class BattleNodeFlowTests await using var _a = clientA; await using var _b = clientB; await Task.WhenAll(clientA.ConsumeHandshakeAsync(ct), clientB.ConsumeHandshakeAsync(ct)); - await DriveHandshakeAsync(clientA, vidA, key, ct); - await DriveHandshakeAsync(clientB, vidB, key, ct); + await DrivePvpHandshakeAsync(clientA, vidA, clientB, vidB, key, ct); // A retires. await clientA.SendMsgAsync(MakeEnvelopeWith(vidA, NetworkBattleUri.Retire, pubSeq: 5), key, ct); @@ -314,8 +312,7 @@ public class BattleNodeFlowTests var (clientA, clientB) = await ConnectBothAsync(factory, pending.BattleId, vidA, vidB, key, ct); await using var _b = clientB; await Task.WhenAll(clientA.ConsumeHandshakeAsync(ct), clientB.ConsumeHandshakeAsync(ct)); - await DriveHandshakeAsync(clientA, vidA, key, ct); - await DriveHandshakeAsync(clientB, vidB, key, ct); + await DrivePvpHandshakeAsync(clientA, vidA, clientB, vidB, key, ct); // Abruptly close A's WS (no Retire). await clientA.DisposeAsync(); @@ -492,7 +489,10 @@ public class BattleNodeFlowTests // -- helpers ------------------------------------------------------------- - private static async Task DriveHandshakeAsync( + /// Drives one PvP client from InitNetwork through Swap, stopping at the + /// SwapResponse. Ready is NOT received here — the mulligan barrier withholds it until + /// BOTH sides have swapped, so the caller drains it after driving both sides. + private static async Task DriveThroughSwapAsync( RawSocketIoTestClient client, long vid, string key, CancellationToken ct) { long pubSeq = 1; @@ -507,7 +507,23 @@ public class BattleNodeFlowTests await client.SendMsgAsync(MakeEnvelopeWith(vid, NetworkBattleUri.Swap, pubSeq++, body: new Dictionary { ["idxList"] = new List() }), key, ct); await client.ReceiveSynchronizeAsync(ct); // Swap response - await client.ReceiveSynchronizeAsync(ct); // Ready + } + + /// Drives both PvP clients through the full handshake including the mulligan + /// barrier: each side swaps first (Ready withheld), then the second swap releases Ready + /// to both. Leaves both at AfterReady with pubSeq up to 4 consumed per client. + private static async Task DrivePvpHandshakeAsync( + RawSocketIoTestClient clientA, long vidA, + RawSocketIoTestClient clientB, long vidB, string key, CancellationToken ct) + { + await DriveThroughSwapAsync(clientA, vidA, key, ct); + await DriveThroughSwapAsync(clientB, vidB, key, ct); + + // B's Swap (the second) releases Ready to both sides. + var aReady = await clientA.ReceiveSynchronizeAsync(ct); + Assert.That(aReady.Uri, Is.EqualTo(NetworkBattleUri.Ready)); + var bReady = await clientB.ReceiveSynchronizeAsync(ct); + Assert.That(bReady.Uri, Is.EqualTo(NetworkBattleUri.Ready)); } private static async Task<(RawSocketIoTestClient, RawSocketIoTestClient)> ConnectBothAsync(