diff --git a/SVSim.UnitTests/BattleNode/Integration/BattleNodeFlowTests.cs b/SVSim.UnitTests/BattleNode/Integration/BattleNodeFlowTests.cs
index 58216cf..46100f8 100644
--- a/SVSim.UnitTests/BattleNode/Integration/BattleNodeFlowTests.cs
+++ b/SVSim.UnitTests/BattleNode/Integration/BattleNodeFlowTests.cs
@@ -1,9 +1,12 @@
+using System.Text.Json;
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.Database;
+using SVSim.Database.Models;
using SVSim.EmulatedEntrypoint;
using SVSim.UnitTests.Infrastructure;
@@ -73,4 +76,86 @@ public class BattleNodeFlowTests
CountryCode: "KOR", UserName: "Player", SleeveId: "3000011",
EmblemId: "701441011", DegreeId: "300003", FieldId: 43, IsOfficial: 0,
BattleType: 11);
+
+ ///
+ /// End-to-end: a viewer with a real TK2 run sees their drafted card-ids in the Matched
+ /// frame's selfDeck. This is the "visible win" — proves the full plumbing chain works
+ /// against an actual seeded viewer.
+ ///
+ [Test]
+ [Timeout(30000)]
+ public async Task Matched_frame_contains_drafted_deck_cards()
+ {
+ await using var factory = new SVSimTestFactory();
+ var vid = await factory.SeedViewerAsync();
+ var draftedDeck = Enumerable.Range(1, 30).Select(i => 200_000_000L + i).ToList();
+
+ using (var seedScope = factory.Services.CreateScope())
+ {
+ var db = seedScope.ServiceProvider.GetRequiredService();
+ db.ViewerArenaTwoPickRuns.Add(new ViewerArenaTwoPickRun
+ {
+ ViewerId = vid,
+ EntryId = 1,
+ ClassId = 1,
+ LeaderSkinId = 1,
+ SelectedCardIdsJson = JsonSerializer.Serialize(draftedDeck),
+ IsSelectCompleted = true,
+ MaxBattleCount = 5,
+ CandidateClassIdsJson = "[1,2,3]",
+ PendingPickSetsJson = "[]",
+ ResultListJson = "[]",
+ NextCandidateId = 1,
+ });
+ await db.SaveChangesAsync();
+ }
+
+ using var scope = factory.Services.CreateScope();
+ var builder = scope.ServiceProvider.GetRequiredService();
+ var ctx = await builder.BuildForTwoPickAsync(vid);
+ var bridge = factory.Services.GetRequiredService();
+
+ using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(15));
+ var ct = cts.Token;
+ var pending = bridge.RegisterPendingBattle(viewerId: vid, context: ctx);
+
+ 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);
+ await client.ReceiveSynchronizeAsync(ct);
+
+ // InitBattle → Matched (this is the frame we care about)
+ await client.SendMsgAsync(MakeEnvelopeWith(vid, NetworkBattleUri.InitBattle, pubSeq: 2), key, ct);
+ var matched = await client.ReceiveSynchronizeAsync(ct);
+ Assert.That(matched.Uri, Is.EqualTo(NetworkBattleUri.Matched));
+
+ // MsgEnvelope.FromJson always inflates Body as a RawBody dictionary — selfDeck is a
+ // List