diff --git a/SVSim.UnitTests/BattleNode/Lifecycle/TypedBodyWireShapeTests.cs b/SVSim.UnitTests/BattleNode/Lifecycle/TypedBodyWireShapeTests.cs
new file mode 100644
index 0000000..3a63a51
--- /dev/null
+++ b/SVSim.UnitTests/BattleNode/Lifecycle/TypedBodyWireShapeTests.cs
@@ -0,0 +1,134 @@
+using System.Text.Json.Nodes;
+using NUnit.Framework;
+using SVSim.BattleNode.Lifecycle;
+using SVSim.BattleNode.Protocol;
+
+namespace SVSim.UnitTests.BattleNode.Lifecycle;
+
+///
+/// Wire-shape regression tests: compare output against
+/// JSON literals derived from captured prod frames in
+/// data_dumps/captures/battle-traffic_tk2_regular.ndjson. Per the feedback_wire_shape_tests
+/// memory, these are literal-comparison tests — NOT self-symmetric round-trips — so they
+/// catch the failure mode where a C# property is renamed and silently breaks the wire
+/// contract.
+///
+[TestFixture]
+public class TypedBodyWireShapeTests
+{
+ [Test]
+ public void BuildMatched_SerializesAllWireKeysExpectedByTheClient()
+ {
+ var env = ScriptedLifecycle.BuildMatched(
+ playerViewerId: 906243102, opponentViewerId: 847666884, battleId: "597830888107");
+
+ var json = MsgEnvelope.ToJson(env);
+ var node = JsonNode.Parse(json)!.AsObject();
+
+ // Top-level envelope fields:
+ Assert.That(node["uri"]!.GetValue(), Is.EqualTo("Matched"));
+ Assert.That(node["viewerId"]!.GetValue(), Is.EqualTo(999_999_999L));
+ Assert.That(node["uuid"]!.GetValue(), Is.EqualTo("node-stub"));
+ Assert.That(node["bid"]!.GetValue(), Is.EqualTo("597830888107"));
+ Assert.That(node["try"]!.GetValue(), Is.EqualTo(0));
+ Assert.That(node["cat"]!.GetValue(), Is.EqualTo(1));
+ Assert.That(node["resultCode"]!.GetValue(), Is.EqualTo(1));
+
+ // Inner selfInfo block has the wire keys the client's Parse looks up.
+ var selfInfo = node["selfInfo"]!.AsObject();
+ foreach (var key in new[] {
+ "country_code", "userName", "sleeveId", "emblemId", "degreeId",
+ "fieldId", "isOfficial", "oppoId", "seed",
+ })
+ {
+ Assert.That(selfInfo.ContainsKey(key), Is.True, $"selfInfo missing wire key '{key}'");
+ }
+ Assert.That(selfInfo["oppoId"]!.GetValue(), Is.EqualTo(847666884L));
+ Assert.That(selfInfo.ContainsKey("oppoDeckCount"), Is.False, "selfInfo must NOT have oppoDeckCount");
+
+ var oppoInfo = node["oppoInfo"]!.AsObject();
+ Assert.That(oppoInfo["oppoDeckCount"]!.GetValue(), Is.EqualTo(30));
+ Assert.That(oppoInfo["oppoId"]!.GetValue(), Is.EqualTo(906243102L));
+
+ var selfDeck = node["selfDeck"]!.AsArray();
+ Assert.That(selfDeck.Count, Is.EqualTo(30));
+ Assert.That(selfDeck[0]!.AsObject()["idx"]!.GetValue(), Is.EqualTo(1));
+ Assert.That(selfDeck[0]!.AsObject()["cardId"]!.GetValue(), Is.EqualTo(100_011_010L));
+ }
+
+ [Test]
+ public void BuildBattleStart_SerializesAllWireKeysAndPreservesBattlePointAsymmetry()
+ {
+ var env = ScriptedLifecycle.BuildBattleStart(playerViewerId: 906243102);
+
+ var json = MsgEnvelope.ToJson(env);
+ var node = JsonNode.Parse(json)!.AsObject();
+
+ Assert.That(node["turnState"]!.GetValue(), Is.EqualTo(0));
+ Assert.That(node["battleType"]!.GetValue(), Is.EqualTo(11));
+ Assert.That(node["resultCode"]!.GetValue(), Is.EqualTo(1));
+
+ var selfInfo = node["selfInfo"]!.AsObject();
+ Assert.That(selfInfo["rank"]!.GetValue(), Is.EqualTo("10"));
+ Assert.That(selfInfo["battlePoint"]!.GetValue(), Is.EqualTo("6270")); // string on self
+ Assert.That(selfInfo["cardMasterName"]!.GetValue(), Is.EqualTo("card_master_node_10015"));
+
+ var oppoInfo = node["oppoInfo"]!.AsObject();
+ Assert.That(oppoInfo["battlePoint"]!.GetValue(), Is.EqualTo(0)); // int on oppo
+ Assert.That(oppoInfo["isMasterRank"]!.GetValue(), Is.EqualTo("0"));
+ Assert.That(oppoInfo["masterPoint"]!.GetValue(), Is.EqualTo("0"));
+ }
+
+ [Test]
+ public void BuildDeal_SerializesSelfAndOppoArraysWithPosIdxShape()
+ {
+ var env = ScriptedLifecycle.BuildDeal();
+ var json = MsgEnvelope.ToJson(env);
+ var node = JsonNode.Parse(json)!.AsObject();
+
+ var self = node["self"]!.AsArray();
+ Assert.That(self.Count, Is.EqualTo(3));
+ Assert.That(self[0]!.AsObject()["pos"]!.GetValue(), Is.EqualTo(0));
+ Assert.That(self[0]!.AsObject()["idx"]!.GetValue(), Is.EqualTo(1));
+
+ var oppo = node["oppo"]!.AsArray();
+ Assert.That(oppo.Count, Is.EqualTo(3));
+ }
+
+ [Test]
+ public void BuildSwapResponse_SerializesSelfWithoutOppo()
+ {
+ var env = ScriptedLifecycle.BuildSwapResponse(new long[] { 1, 4, 3 });
+ var json = MsgEnvelope.ToJson(env);
+ var node = JsonNode.Parse(json)!.AsObject();
+
+ Assert.That(node.ContainsKey("self"), Is.True);
+ Assert.That(node.ContainsKey("oppo"), Is.False);
+ Assert.That(node["self"]!.AsArray()[1]!.AsObject()["idx"]!.GetValue(), Is.EqualTo(4));
+ }
+
+ [Test]
+ public void BuildReady_SerializesAllFieldsIncludingSeedAndSpin()
+ {
+ var env = ScriptedLifecycle.BuildReady(new long[] { 1, 4, 3 });
+ var json = MsgEnvelope.ToJson(env);
+ var node = JsonNode.Parse(json)!.AsObject();
+
+ Assert.That(node["idxChangeSeed"]!.GetValue(), Is.EqualTo(771_335_280));
+ Assert.That(node["spin"]!.GetValue(), Is.EqualTo(243));
+ Assert.That(node["self"]!.AsArray().Count, Is.EqualTo(3));
+ Assert.That(node["oppo"]!.AsArray().Count, Is.EqualTo(3));
+ }
+
+ [Test]
+ public void BuildOpponentTurnStart_SerializesSpinAndResultCode()
+ {
+ var env = ScriptedLifecycle.BuildOpponentTurnStart();
+ var json = MsgEnvelope.ToJson(env);
+ var node = JsonNode.Parse(json)!.AsObject();
+
+ Assert.That(node["spin"]!.GetValue(), Is.EqualTo(100));
+ Assert.That(node["resultCode"]!.GetValue(), Is.EqualTo(1));
+ Assert.That(node["uri"]!.GetValue(), Is.EqualTo("TurnStart"));
+ }
+}