Pushing Matched in response to InitNetwork lands it before
MatchingInitBattle() finishes wiring up the OnReceivedEvent handler
and setting status=Connect. The client's Matched-case in
ReactionReceiveUri only transitions to StartLoad when status is
Connect at the moment of receipt; otherwise the frame is silently
dropped at the state machine and the matchmaking UI never advances.
The real connect-handshake sequence (per MatchingNetworkConnectChecker
+ Matching.cs):
1. WS opens.
2. Client emits InitNetwork (cat=general).
3. Server replies InitNetwork ack → _initNetworkSuccess = true.
4. MatchingInitBattle: status=Connect; emit InitBattle; subscribe
OnReceivedEvent matching handler.
5. Server replies Matched → status=StartLoad, StartBattleLoad.
6. Asset load done → client emits Loaded.
7. Server replies BattleStart + Deal → status=Prepared, GotoBattle.
Add AwaitingInitBattle phase, gate Matched on InitBattle receipt, and
gate BattleStart+Deal on Loaded receipt. Update dispatch and
integration tests to walk the new sequence; InitBattle's wire cat is
Matching(2), not Battle(1).
Caught during v1 smoke walkthrough — battle-traffic.ndjson showed the
client receiving Matched/BattleStart at sub-millisecond gaps after
InitNetwork ack, but never advancing past matchmaking.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
77 lines
3.9 KiB
C#
77 lines
3.9 KiB
C#
using Microsoft.AspNetCore.Mvc.Testing;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using NUnit.Framework;
|
|
using SVSim.BattleNode.Bridge;
|
|
using SVSim.BattleNode.Protocol;
|
|
using SVSim.BattleNode.Wire;
|
|
using SVSim.EmulatedEntrypoint;
|
|
using SVSim.UnitTests.Infrastructure;
|
|
|
|
namespace SVSim.UnitTests.BattleNode.Integration;
|
|
|
|
[TestFixture]
|
|
public class BattleNodeFlowTests
|
|
{
|
|
/// <summary>
|
|
/// End-to-end smoke for the v1 scripted lifecycle. Boots the EmulatedEntrypoint via
|
|
/// SVSimTestFactory (in-memory SQLite + reference-data CSV import), mints a battle
|
|
/// through IMatchingBridge, opens a raw Socket.IO v2 client against the in-process
|
|
/// TestServer, and drives InitNetwork → Loaded → Swap, asserting the right scripted
|
|
/// frames come back in order.
|
|
/// </summary>
|
|
[Test]
|
|
[Timeout(30000)]
|
|
public async Task ClientWalksHandshakeToReady_ReceivesAllScriptedFrames()
|
|
{
|
|
await using var factory = new SVSimTestFactory();
|
|
var bridge = factory.Services.GetRequiredService<IMatchingBridge>();
|
|
|
|
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(15));
|
|
var ct = cts.Token;
|
|
var pending = bridge.RegisterPendingBattle(viewerId: 906243102);
|
|
|
|
var key = MakeKey();
|
|
var encryptedVid = NodeCrypto.EncryptForNode("906243102", key);
|
|
// TestServer ignores the host portion of the URI — only the path + query route.
|
|
var wsUri = new Uri($"ws://localhost/socket.io/?BattleId={pending.BattleId}&viewerId={Uri.EscapeDataString(encryptedVid)}&EIO=3&transport=websocket");
|
|
|
|
var wsClient = factory.Server.CreateWebSocketClient();
|
|
var ws = await wsClient.ConnectAsync(wsUri, ct);
|
|
await using var client = new RawSocketIoTestClient(ws);
|
|
await client.ConsumeHandshakeAsync(ct);
|
|
|
|
// 1. InitNetwork → expect InitNetwork ack push only.
|
|
await client.SendMsgAsync(MakeEnvelope(NetworkBattleUri.InitNetwork, pubSeq: 1), key, ct);
|
|
Assert.That((await client.ReceiveSynchronizeAsync(ct)).Uri, Is.EqualTo(NetworkBattleUri.InitNetwork));
|
|
|
|
// 2. InitBattle → expect Matched (handler is now subscribed on the client side).
|
|
await client.SendMsgAsync(MakeEnvelope(NetworkBattleUri.InitBattle, pubSeq: 2), key, ct);
|
|
Assert.That((await client.ReceiveSynchronizeAsync(ct)).Uri, Is.EqualTo(NetworkBattleUri.Matched));
|
|
|
|
// 3. Loaded → expect BattleStart + Deal.
|
|
await client.SendMsgAsync(MakeEnvelope(NetworkBattleUri.Loaded, pubSeq: 3), key, ct);
|
|
Assert.That((await client.ReceiveSynchronizeAsync(ct)).Uri, Is.EqualTo(NetworkBattleUri.BattleStart));
|
|
Assert.That((await client.ReceiveSynchronizeAsync(ct)).Uri, Is.EqualTo(NetworkBattleUri.Deal));
|
|
|
|
// 4. Swap with empty idxList → expect Swap response + Ready.
|
|
await client.SendMsgAsync(MakeEnvelope(NetworkBattleUri.Swap, pubSeq: 4,
|
|
body: new Dictionary<string, object?> { ["idxList"] = new List<object?>() }), key, ct);
|
|
Assert.That((await client.ReceiveSynchronizeAsync(ct)).Uri, Is.EqualTo(NetworkBattleUri.Swap));
|
|
Assert.That((await client.ReceiveSynchronizeAsync(ct)).Uri, Is.EqualTo(NetworkBattleUri.Ready));
|
|
}
|
|
|
|
private static MsgEnvelope MakeEnvelope(NetworkBattleUri uri, long pubSeq, Dictionary<string, object?>? body = null) =>
|
|
new(uri, ViewerId: 906243102, Uuid: "udid-test", Bid: null, Try: 0,
|
|
// EmitMsgPack: InitNetwork → general(99); other matching URIs → matching(2); else battle(1).
|
|
Cat: uri == NetworkBattleUri.InitNetwork ? EmitCategory.General
|
|
: uri == NetworkBattleUri.InitBattle ? EmitCategory.Matching
|
|
: EmitCategory.Battle,
|
|
PubSeq: pubSeq, PlaySeq: null, Body: body ?? new Dictionary<string, object?>());
|
|
|
|
private static string MakeKey()
|
|
{
|
|
var seq = 0;
|
|
return NodeCrypto.GenerateKey(() => (seq++ * 13) % 16);
|
|
}
|
|
}
|