test(battle-node): BotBattle_FullLifecycle integration test
Single-client end-to-end Bot lifecycle: InitNetwork → ack, InitBattle → ack (no Matched), Loaded → silent, Swap → SwapResponse + Ready, two TurnEnd cycles each producing a single Judge frame back to sender, Retire → BattleFinish. Pending battle evicted at session start. Closes Phase 3 — battle-node v2's three-phase migration (Scripted → PvP → Bot) is now complete. Test budget: 884 → 931 (+47 across Phase 3). Next: matching-queue API rewrite + real rank progression, as separate specs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -399,6 +399,85 @@ public class BattleNodeFlowTests
|
||||
Assert.That(store.TryGetPending(pending.BattleId), Is.Null);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Bot integration test (Task 13). Single client, full Bot lifecycle: handshake
|
||||
// through Swap (asserting NO Matched / BattleStart / Deal pushes), TurnEnd cycles
|
||||
// each producing a single Judge back, Retire → BattleFinish. Reference:
|
||||
// docs/api-spec/in-battle/ai-passive.md.
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
[Test]
|
||||
[Timeout(30000)]
|
||||
public async Task BotBattle_FullLifecycle()
|
||||
{
|
||||
await using var factory = new SVSimTestFactory();
|
||||
var vid = await factory.SeedViewerAsync();
|
||||
await factory.SeedGlobalsAsync();
|
||||
await factory.SeedDeckAsync(vid, SVSim.Database.Enums.Format.Rotation, 1);
|
||||
|
||||
var bridge = factory.Services.GetRequiredService<IMatchingBridge>();
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var builder = scope.ServiceProvider.GetRequiredService<SVSim.EmulatedEntrypoint.Services.IMatchContextBuilder>();
|
||||
var ctx = await builder.BuildForRankBattleAsync(vid, SVSim.Database.Enums.Format.Rotation);
|
||||
var pending = bridge.RegisterBattle(
|
||||
new SVSim.BattleNode.Bridge.BattlePlayer(vid, ctx),
|
||||
p2: null,
|
||||
SVSim.BattleNode.Sessions.BattleType.Bot);
|
||||
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(15));
|
||||
var ct = cts.Token;
|
||||
var key = MakeKey();
|
||||
var encryptedVid = NodeCrypto.EncryptForNode(vid.ToString(), key);
|
||||
var wsUri = new Uri($"ws://localhost/socket.io/?BattleId={pending.BattleId}&viewerId={Uri.EscapeDataString(encryptedVid)}&EIO=3&transport=websocket");
|
||||
|
||||
var wsClient = factory.Server.CreateWebSocketClient();
|
||||
var ws = await wsClient.ConnectAsync(wsUri, ct);
|
||||
await using var client = new RawSocketIoTestClient(ws);
|
||||
await client.ConsumeHandshakeAsync(ct);
|
||||
|
||||
// InitNetwork → ack.
|
||||
await client.SendMsgAsync(MakeEnvelopeWith(vid, NetworkBattleUri.InitNetwork, pubSeq: 1), key, ct);
|
||||
var ack1 = await client.ReceiveSynchronizeAsync(ct);
|
||||
Assert.That(ack1.Uri, Is.EqualTo(NetworkBattleUri.InitNetwork));
|
||||
|
||||
// InitBattle → ack (NOT Matched).
|
||||
await client.SendMsgAsync(MakeEnvelopeWith(vid, NetworkBattleUri.InitBattle, pubSeq: 2), key, ct);
|
||||
var ack2 = await client.ReceiveSynchronizeAsync(ct);
|
||||
Assert.That(ack2.Uri, Is.EqualTo(NetworkBattleUri.InitBattle),
|
||||
"Bot's InitBattle is ack-only — no Matched envelope.");
|
||||
|
||||
// Loaded → silent. Send Swap right after; the next inbound must be SwapResponse
|
||||
// (no orphan BattleStart / Deal in the queue).
|
||||
await client.SendMsgAsync(MakeEnvelopeWith(vid, NetworkBattleUri.Loaded, pubSeq: 3), key, ct);
|
||||
await client.SendMsgAsync(MakeEnvelopeWith(vid, NetworkBattleUri.Swap, pubSeq: 4,
|
||||
body: new Dictionary<string, object?> { ["idxList"] = new List<object?>() }), key, ct);
|
||||
var swapResp = await client.ReceiveSynchronizeAsync(ct);
|
||||
Assert.That(swapResp.Uri, Is.EqualTo(NetworkBattleUri.Swap),
|
||||
"Expected Swap response (mulligan ack). Got " + swapResp.Uri + " — Loaded may have leaked a frame.");
|
||||
var readyResp = await client.ReceiveSynchronizeAsync(ct);
|
||||
Assert.That(readyResp.Uri, Is.EqualTo(NetworkBattleUri.Ready));
|
||||
|
||||
// TurnEnd → Judge back.
|
||||
await client.SendMsgAsync(MakeEnvelopeWith(vid, NetworkBattleUri.TurnEnd, pubSeq: 5), key, ct);
|
||||
var judge1 = await client.ReceiveSynchronizeAsync(ct);
|
||||
Assert.That(judge1.Uri, Is.EqualTo(NetworkBattleUri.Judge));
|
||||
|
||||
// Second TurnEnd → another Judge back.
|
||||
await client.SendMsgAsync(MakeEnvelopeWith(vid, NetworkBattleUri.TurnEnd, pubSeq: 6), key, ct);
|
||||
var judge2 = await client.ReceiveSynchronizeAsync(ct);
|
||||
Assert.That(judge2.Uri, Is.EqualTo(NetworkBattleUri.Judge));
|
||||
|
||||
// Retire → BattleFinish.
|
||||
await client.SendMsgAsync(MakeEnvelopeWith(vid, NetworkBattleUri.Retire, pubSeq: 7), key, ct);
|
||||
var finish = await client.ReceiveSynchronizeAsync(ct);
|
||||
Assert.That(finish.Uri, Is.EqualTo(NetworkBattleUri.BattleFinish));
|
||||
|
||||
// PendingBattle evicted at session start.
|
||||
var store = factory.Services.GetRequiredService<SVSim.BattleNode.Sessions.IBattleSessionStore>();
|
||||
Assert.That(store.TryGetPending(pending.BattleId), Is.Null,
|
||||
"PendingBattle should be evicted at session start.");
|
||||
}
|
||||
|
||||
// -- helpers -------------------------------------------------------------
|
||||
|
||||
private static async Task DriveHandshakeAsync(
|
||||
|
||||
Reference in New Issue
Block a user