fix(battle-node): preserve long type on numeric array elements in FromJson

Root cause for the lingering mulligan failure: the inline conditional
expression in MsgEnvelope.ToObject

    JsonValueKind.Number => el.TryGetInt64(out var l) ? l : el.GetDouble(),

unified its branches to the common implicit-convertible type. long→double
is implicit, so both branches collapsed to double and the integer value
silently widened. Inside an array (idxList:[2]), each element came back
as boxed double; OfType<long> in ExtractIdxList then filtered every
entry out, so swapIndices arrived empty and BuildSwapResponse echoed
the unchanged hand — exactly the diff-against-Deal mismatch the client
flagged as "Card swap failed: AbandonCards[2]/DrawCards[]".

Extract a ParseNumber helper that returns object explicitly so each
branch boxes its own runtime type. Also harden ExtractIdxList to accept
any boxed numeric type (long/int/double/decimal/string) so a future
JSON-parser drift can't silently regress this path again.

Two regression tests:
- FromJson_NumericArray_PreservesLongTypeOnEachElement: confirms the
  fix at the JSON-parse layer with a hardcoded "{\"idxList\":[2,3]}".
- Swap_WithIdxListContainingTwo_ProducesHandWithFreshIdxAtPosition1:
  exercises the dispatch end-to-end with a Body holding a real boxed
  long; asserts position 1 of the response hand is the fresh deck idx 4.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
gamer147
2026-06-01 08:40:50 -04:00
parent 77fb93f3ea
commit 9e8ebd1b2b
4 changed files with 75 additions and 3 deletions

View File

@@ -7,6 +7,26 @@ namespace SVSim.UnitTests.BattleNode.Protocol;
[TestFixture]
public class MsgEnvelopeTests
{
[Test]
public void FromJson_NumericArray_PreservesLongTypeOnEachElement()
{
// Regression for the v1 mulligan bug: idxList:[2] from the wire was decoded as a list
// containing a boxed double (2.0) instead of a boxed long (2L). The conditional expression
// `el.TryGetInt64(out var l) ? l : el.GetDouble()` unified its branches to the common
// implicit type (double) and silently widened the long. Downstream OfType<long> filtered
// every entry out, so swapIndices arrived empty and the server echoed the unchanged hand.
const string json = "{\"uri\":\"Swap\",\"viewerId\":1,\"uuid\":\"u\",\"try\":0,\"cat\":1,\"idxList\":[2,3]}";
var env = MsgEnvelope.FromJson(json);
var idxList = (List<object?>)env.Body["idxList"]!;
Assert.That(idxList.Count, Is.EqualTo(2));
Assert.That(idxList[0], Is.TypeOf<long>(), "idxList[0] must be boxed long, not double.");
Assert.That(idxList[0], Is.EqualTo(2L));
Assert.That(idxList[1], Is.TypeOf<long>());
Assert.That(idxList[1], Is.EqualTo(3L));
}
[Test]
public void Roundtrip_PreservesEnvelopeAndBody()
{