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 <noreply@anthropic.com>
This commit is contained in:
gamer147
2026-06-03 10:58:33 -04:00
parent feb387d3d5
commit afe2984075

View File

@@ -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(
/// <summary>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.</summary>
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<string, object?> { ["idxList"] = new List<object?>() }), key, ct);
await client.ReceiveSynchronizeAsync(ct); // Swap response
await client.ReceiveSynchronizeAsync(ct); // Ready
}
/// <summary>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.</summary>
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(