Files
SVSimServer/SVSim.UnitTests/BattleNode/Sessions/BattleSessionDispatchTests.cs
gamer147 680630050b fix(battle-node): BattleSession crash safety, fresh-key per push, phase guards
- Wrap HandleMsgEventAsync / HandleAliveEventAsync bodies in try/catch(Exception)
  logging at Error, eliminating async-void unobserved-exception crash risk (Issue 1).
- Replace deterministic seq-based key generator with RandomNumberGenerator.GetInt32
  so each EncodeAndSendAsync call uses a fresh random key (Issue 2).
- Add `when Phase == …` guards to InitNetwork / Loaded / Swap cases in
  ComputeResponses; add default arm that logs+drops out-of-order URIs (Issue 3).
- Widen SendSioAckAsync arg from int to long; drop (int) cast at call site;
  boundary cast to int is now checked() for defensive overflow detection (Issue 4).
- Update RunAsync doc comment (was stale Task-13 placeholder) (Issue 5).
- Add Kill and out-of-order-Swap-before-Loaded tests (Issue 6).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 22:28:13 -04:00

86 lines
3.5 KiB
C#

// SVSim.UnitTests/BattleNode/Sessions/BattleSessionDispatchTests.cs
using Microsoft.Extensions.Logging.Abstractions;
using NUnit.Framework;
using SVSim.BattleNode.Protocol;
using SVSim.BattleNode.Sessions;
namespace SVSim.UnitTests.BattleNode.Sessions;
[TestFixture]
public class BattleSessionDispatchTests
{
private static BattleSession NewSession()
{
// ws is unused by ComputeResponses; pass null! and rely on the test never invoking the pump.
return new BattleSession(ws: null!, battleId: "bid-1", viewerId: 1, log: NullLogger<BattleSession>.Instance);
}
private static MsgEnvelope NewEnvelope(NetworkBattleUri uri) =>
new(uri, ViewerId: 1, Uuid: "u", Bid: null, Try: 0, Cat: EmitCategory.Battle,
PubSeq: null, PlaySeq: null, Body: new Dictionary<string, object?>());
[Test]
public void InitNetwork_PushesAckThenMatchedThenBattleStart()
{
var s = NewSession();
var responses = s.ComputeResponses(NewEnvelope(NetworkBattleUri.InitNetwork));
Assert.That(responses.Select(r => r.Envelope.Uri),
Is.EqualTo(new[] { NetworkBattleUri.InitNetwork, NetworkBattleUri.Matched, NetworkBattleUri.BattleStart }));
Assert.That(s.Phase, Is.EqualTo(BattleSessionPhase.AwaitingLoaded));
}
[Test]
public void Loaded_PushesDeal_TransitionsToAwaitingSwap()
{
var s = NewSession();
s.ComputeResponses(NewEnvelope(NetworkBattleUri.InitNetwork)); // advance phase
var responses = s.ComputeResponses(NewEnvelope(NetworkBattleUri.Loaded));
Assert.That(responses.Single().Envelope.Uri, Is.EqualTo(NetworkBattleUri.Deal));
Assert.That(s.Phase, Is.EqualTo(BattleSessionPhase.AwaitingSwap));
}
[Test]
public void Swap_PushesSwapResponseThenReady_TransitionsToAfterReady()
{
var s = NewSession();
s.ComputeResponses(NewEnvelope(NetworkBattleUri.InitNetwork));
s.ComputeResponses(NewEnvelope(NetworkBattleUri.Loaded));
var responses = s.ComputeResponses(NewEnvelope(NetworkBattleUri.Swap));
Assert.That(responses.Select(r => r.Envelope.Uri),
Is.EqualTo(new[] { NetworkBattleUri.Swap, NetworkBattleUri.Ready }));
Assert.That(s.Phase, Is.EqualTo(BattleSessionPhase.AfterReady));
}
[Test]
public void Retire_PushesBattleFinishNoContest_TransitionsToTerminal()
{
var s = NewSession();
var responses = s.ComputeResponses(NewEnvelope(NetworkBattleUri.Retire));
var (env, noStock) = responses.Single();
Assert.That(env.Uri, Is.EqualTo(NetworkBattleUri.BattleFinish));
Assert.That(noStock, Is.True);
Assert.That(s.Phase, Is.EqualTo(BattleSessionPhase.Terminal));
}
[Test]
public void Kill_PushesBattleFinishNoContest_TransitionsToTerminal()
{
var s = NewSession();
var responses = s.ComputeResponses(NewEnvelope(NetworkBattleUri.Kill));
var (env, noStock) = responses.Single();
Assert.That(env.Uri, Is.EqualTo(NetworkBattleUri.BattleFinish));
Assert.That(noStock, Is.True);
Assert.That(s.Phase, Is.EqualTo(BattleSessionPhase.Terminal));
}
[Test]
public void Swap_ArrivingBeforeLoaded_ProducesNoResponseAndDoesNotAdvancePhase()
{
var s = NewSession();
// Skip Loaded — fire Swap straight out of AwaitingInitNetwork.
var responses = s.ComputeResponses(NewEnvelope(NetworkBattleUri.Swap));
Assert.That(responses, Is.Empty);
Assert.That(s.Phase, Is.EqualTo(BattleSessionPhase.AwaitingInitNetwork));
}
}