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")); + } +}