diff --git a/SVSim.UnitTests/BattleNode/Integration/BattleNodeFlowTests.cs b/SVSim.UnitTests/BattleNode/Integration/BattleNodeFlowTests.cs
new file mode 100644
index 0000000..2d58e42
--- /dev/null
+++ b/SVSim.UnitTests/BattleNode/Integration/BattleNodeFlowTests.cs
@@ -0,0 +1,73 @@
+using Microsoft.AspNetCore.Mvc.Testing;
+using Microsoft.Extensions.DependencyInjection;
+using NUnit.Framework;
+using SVSim.BattleNode.Bridge;
+using SVSim.BattleNode.Protocol;
+using SVSim.BattleNode.Wire;
+using SVSim.EmulatedEntrypoint;
+using SVSim.UnitTests.Infrastructure;
+
+namespace SVSim.UnitTests.BattleNode.Integration;
+
+[TestFixture]
+public class BattleNodeFlowTests
+{
+ ///
+ /// End-to-end smoke for the v1 scripted lifecycle. Boots the EmulatedEntrypoint via
+ /// SVSimTestFactory (in-memory SQLite + reference-data CSV import), mints a battle
+ /// through IMatchingBridge, opens a raw Socket.IO v2 client against the in-process
+ /// TestServer, and drives InitNetwork → Loaded → Swap, asserting the right scripted
+ /// frames come back in order.
+ ///
+ [Test]
+ [Timeout(30000)]
+ public async Task ClientWalksHandshakeToReady_ReceivesAllScriptedFrames()
+ {
+ await using var factory = new SVSimTestFactory();
+ var bridge = factory.Services.GetRequiredService();
+
+ using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(15));
+ var ct = cts.Token;
+ var pending = bridge.RegisterPendingBattle(viewerId: 906243102);
+
+ var key = MakeKey();
+ var encryptedVid = NodeCrypto.EncryptForNode("906243102", key);
+ // TestServer ignores the host portion of the URI — only the path + query route.
+ 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);
+
+ // 1. InitNetwork → expect InitNetwork ack push, then Matched, then BattleStart.
+ await client.SendMsgAsync(MakeEnvelope(NetworkBattleUri.InitNetwork, pubSeq: 1), key, ct);
+ var f1 = await client.ReceiveSynchronizeAsync(ct);
+ var f2 = await client.ReceiveSynchronizeAsync(ct);
+ var f3 = await client.ReceiveSynchronizeAsync(ct);
+ Assert.That(f1.Uri, Is.EqualTo(NetworkBattleUri.InitNetwork));
+ Assert.That(f2.Uri, Is.EqualTo(NetworkBattleUri.Matched));
+ Assert.That(f3.Uri, Is.EqualTo(NetworkBattleUri.BattleStart));
+
+ // 2. Loaded → expect Deal.
+ await client.SendMsgAsync(MakeEnvelope(NetworkBattleUri.Loaded, pubSeq: 2), key, ct);
+ Assert.That((await client.ReceiveSynchronizeAsync(ct)).Uri, Is.EqualTo(NetworkBattleUri.Deal));
+
+ // 3. Swap with empty idxList → expect Swap response + Ready.
+ await client.SendMsgAsync(MakeEnvelope(NetworkBattleUri.Swap, pubSeq: 3,
+ body: new Dictionary { ["idxList"] = new List