From afe2984075f420dda40107b73e7d83c0ef02fe06 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Wed, 3 Jun 2026 10:58:33 -0400 Subject: [PATCH] test(battle-node): drive PvP flow handshakes through the mulligan barrier The three PvP BattleNodeFlowTests drove each client's handshake to Ready independently; the new barrier withholds Ready until both sides swap, so the single-client helper timed out. Split DriveHandshakeAsync into DriveThroughSwapAsync (stops at SwapResponse) + DrivePvpHandshakeAsync (drives both, then drains the barrier-released Ready for each). Scripted/Bot single-client paths are unaffected (non-IHasHandshakePhase opponent releases Ready immediately). Co-Authored-By: Claude Opus 4.8 --- .../Integration/BattleNodeFlowTests.cs | 32 ++++++++++++++----- 1 file changed, 24 insertions(+), 8 deletions(-) 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(