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