From b73f0f7157c0cd6f8e22ed29fda5d220ed9d2a3c Mon Sep 17 00:00:00 2001 From: gamer147 Date: Sat, 6 Jun 2026 21:01:01 -0400 Subject: [PATCH] test(battlenode): reveal test stresses cardId substitution with mismatched seed (M-HC-2 review) Co-Authored-By: Claude Opus 4.8 --- .../Integration/HeadlessConductorTests.cs | 71 +++++++++++++++++-- 1 file changed, 64 insertions(+), 7 deletions(-) diff --git a/SVSim.UnitTests/BattleNode/Integration/HeadlessConductorTests.cs b/SVSim.UnitTests/BattleNode/Integration/HeadlessConductorTests.cs index fed1845..41f2c1d 100644 --- a/SVSim.UnitTests/BattleNode/Integration/HeadlessConductorTests.cs +++ b/SVSim.UnitTests/BattleNode/Integration/HeadlessConductorTests.cs @@ -224,13 +224,16 @@ public class HeadlessConductorTests Is.True, "turn2 TurnStart (seat B active)"); // Seat B's opening hand is hidden (deck reads full minus its single turn-1 draw); its cards - // have NOT been disclosed to the relay yet. The dummy at engine Index 1 is the deck-idx-1 card - // (the vanilla follower), seated in a hidden zone — NOT on the board. Confirm seat B's board is - // empty BEFORE the reveal, so the post-reveal +1 is decisively the reveal seating the card. - // (Node-native, the harness seeds each side's cards with their real id rather than cardId-0 - // dummies — it knows both decks — so the reveal substitution is identity-preserving here; the - // board delta is what proves ReplaceReceivedCard.ReplaceCard -> CreateActualCard resolved the - // card onto the board headless. M-HC-5 exercises a true cardId-0 -> cardId substitution.) + // have NOT been disclosed to the relay yet. The dummy at engine Index 1 is whatever card the + // shuffle seated at that index (shuffledDeck[0]), parked in a hidden zone — NOT on the board. + // Confirm seat B's board is empty BEFORE the reveal, so the post-reveal +1 is decisively the + // reveal seating the card. (Node-native, the harness seeds each side's cards with their real id + // — it knows both decks — so this test's reveal substitution is identity-preserving by choice; + // CreateActualCard builds the card purely from the wire cardId regardless of which card the + // shuffle parked at Index 1. The board delta is what proves ReplaceReceivedCard.ReplaceCard -> + // CreateActualCard resolved the card onto the board headless. The companion test + // Opponent_reveal_overrides_seeded_identity_headless stresses a MISMATCHED cardId to prove the + // wire id — not the seeded identity — is what gets seated.) var boardBefore = harness.BoardCount(playerSeat: false); Assert.That(boardBefore, Is.EqualTo(0), "seat B has no board followers before the reveal"); @@ -247,6 +250,60 @@ public class HeadlessConductorTests "the seated card's identity must equal the wire cardId from the reveal"); } + [Test] + public void Opponent_reveal_overrides_seeded_identity_headless() + { + // This is the substitution half of M-HC-2: prove the seated card's POST-reveal identity is the + // WIRE cardId even when it DIFFERS from whatever the shuffle parked at that engine Index. + // ReplaceReceivedCard.CreateActualCard builds the card purely from cardData.CardId, independent + // of the seated dummy's id — so a reveal whose cardId mismatches the seed must OVERRIDE it. + // + // Z (seeded) vs W (revealed) are DIFFERENT cost-1 vanilla followers, both present + creatable in + // cards.json: + // Z = 100011010 — the proven vanilla follower (char_type 1, cost 1). Seat B's deck is made + // UNIFORMLY of Z, so whichever idx the shuffle parked at Index 1 is unambiguously Z. + // W = 101211120 — a different cost-1 vanilla follower (char_type 1, cost 1, no skill). Cost 1 + // seats at seat B's first-turn PP (1). The id is NOT in seat B's deck, so the only way it + // can appear on the board is the reveal substituting it in. + const long Z = NodeNativeBattleHarness.VanillaFollowerId; // 100011010 + const long W = 101211120; + Assert.That(W, Is.Not.EqualTo(Z), "Z and W must differ for the substitution to be observable"); + + // Uniform Z deck for seat B (every dummy is Z regardless of shuffle). Seat A left at default. + var seatBDeck = Enumerable.Repeat(Z, 30).ToList(); + + using var harness = NodeNativeBattleHarness.Create(seatBDeck: seatBDeck); + + // --- drive to seat B's turn (same two-turn sequence as the sibling reveal test) ------------- + Assert.That(harness.Push(NetworkBattleUri.Deal, DealBody(), isPlayerSeat: true).Accepted, + Is.True, "Deal"); + Assert.That(harness.Push(NetworkBattleUri.Swap, SwapBody(), isPlayerSeat: true).Accepted, + Is.True, "Swap"); + Assert.That(harness.Push(NetworkBattleUri.Ready, ReadyBody(), isPlayerSeat: true).Accepted, + Is.True, "Ready"); + Assert.That(harness.Push(NetworkBattleUri.TurnStart, TurnStartBody(), isPlayerSeat: true).Accepted, + Is.True, "turn1 TurnStart"); + Assert.That(harness.Push(NetworkBattleUri.TurnEnd, TurnEndBody(), isPlayerSeat: true).Accepted, + Is.True, "turn1 TurnEnd"); + Assert.That(harness.Push(NetworkBattleUri.TurnStart, TurnStartBody(), isPlayerSeat: false).Accepted, + Is.True, "turn2 TurnStart (seat B active)"); + + var boardBefore = harness.BoardCount(playerSeat: false); + Assert.That(boardBefore, Is.EqualTo(0), "seat B has no board followers before the reveal"); + + // The reveal discloses idx 1 (seeded as Z) with the MISMATCHED wire cardId W. + var reveal = harness.Push( + NetworkBattleUri.PlayActions, RevealPlayBody(idx: 1, cardId: W), isPlayerSeat: false); + + Assert.That(reveal.Accepted, Is.True, $"opponent reveal rejected: {reveal.RejectReason}"); + Assert.That(harness.BoardCount(playerSeat: false), Is.EqualTo(boardBefore + 1), + "the revealed follower must seat on seat B's board"); + // The decisive assertion: the seated identity is W (the wire cardId), NOT Z (the seeded id). + // Because the deck is uniformly Z, this can only pass if the reveal OVERRODE the seeded identity. + Assert.That(harness.InPlayCardId(playerSeat: false, boardPos: 0), Is.EqualTo((int)W), + "the seated card must be the wire cardId W, overriding the seeded Z identity at that idx"); + } + [Test] public void Deal_seats_three_card_hand_headless() {