Files
SVSimServer/SVSim.UnitTests/Matching/InProcessPairUpTests.cs
gamer147 8112b3f81f feat(arena-tk2): split do_matching success into 3007 owner / 3004 joiner
Mirrors prod's TK2 wire flow: the first arriver (parked, picks up cached
pair on a later poll) gets matching_state 3007 (SUCCEEDED_OWNER); the
second arriver (whose poll triggered the pair) gets 3004 (SUCCEEDED).

Observationally inert in the public matching code path today — the
client's Matching class writes isOwner from the response into a field
that nothing in TK2/ranked reads. Matching_Room (private rooms) DOES
read it but from a separate code path that doesn't consult our response.
We send the split anyway for prod fidelity and to leave room for future
flows (rematch UI, etc.) that might start consuming it.

TryPairAsync now returns PairUpResult(Match, IsOwner) instead of bare
PendingMatch?, so the controller can decide owner vs joiner without
re-deriving it.

Also documents on DoMatchingResponseDto why we omit prod's `room_id`
field (not in the client's DoMatchingDetail model; private-room flows
get their room id from a different API and don't consult this response).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 23:24:13 -04:00

76 lines
3.5 KiB
C#

using NUnit.Framework;
using SVSim.BattleNode.Bridge;
using SVSim.BattleNode.Sessions;
using SVSim.EmulatedEntrypoint.Matching;
namespace SVSim.UnitTests.Matching;
[TestFixture]
public class InProcessPairUpTests
{
[Test]
public async Task TryPairAsync_on_empty_slot_returns_null_and_parks()
{
var bridge = new MatchingBridge(new InMemoryBattleSessionStore(), new BattleNodeOptions());
var svc = new InProcessPairUp(bridge);
var match = await svc.TryPairAsync("tk2", new BattlePlayer(1, Ctx()), CancellationToken.None);
Assert.That(match, Is.Null);
}
[Test]
public async Task TryPairAsync_with_waiting_partner_pairs_returns_match_as_joiner()
{
var bridge = new MatchingBridge(new InMemoryBattleSessionStore(), new BattleNodeOptions());
var svc = new InProcessPairUp(bridge);
await svc.TryPairAsync("tk2", new BattlePlayer(1, Ctx()), CancellationToken.None);
var result = await svc.TryPairAsync("tk2", new BattlePlayer(2, Ctx()), CancellationToken.None);
Assert.That(result, Is.Not.Null);
Assert.That(result!.Match.BattleId, Is.Not.Empty);
Assert.That(result.IsOwner, Is.False,
"The second arriver (who triggered the pair) is the joiner — wire matching_state 3004.");
}
[Test]
public async Task First_arrivers_next_poll_returns_cached_match_as_owner_then_evicts()
{
var bridge = new MatchingBridge(new InMemoryBattleSessionStore(), new BattleNodeOptions());
var svc = new InProcessPairUp(bridge);
await svc.TryPairAsync("tk2", new BattlePlayer(1, Ctx()), CancellationToken.None); // park
var secondPaired = await svc.TryPairAsync("tk2", new BattlePlayer(2, Ctx()), CancellationToken.None); // pair
var firstCached = await svc.TryPairAsync("tk2", new BattlePlayer(1, Ctx()), CancellationToken.None); // consume
var firstAgain = await svc.TryPairAsync("tk2", new BattlePlayer(1, Ctx()), CancellationToken.None); // post-consume
Assert.That(firstCached, Is.Not.Null);
Assert.That(firstCached!.Match.BattleId, Is.EqualTo(secondPaired!.Match.BattleId));
Assert.That(firstCached.IsOwner, Is.True,
"The first arriver picking up their cached pair is the owner — wire matching_state 3007.");
Assert.That(secondPaired.IsOwner, Is.False,
"Sanity: the same pair-up returns IsOwner=true to the cached/first arriver and IsOwner=false to the joiner.");
Assert.That(firstAgain, Is.Null, "Consumed entry must be evicted; next call re-parks.");
}
[Test]
public async Task Different_modes_do_not_pair_across_slots()
{
var bridge = new MatchingBridge(new InMemoryBattleSessionStore(), new BattleNodeOptions());
var svc = new InProcessPairUp(bridge);
await svc.TryPairAsync("tk2", new BattlePlayer(1, Ctx()), CancellationToken.None);
var rankMatch = await svc.TryPairAsync("rank_rotation", new BattlePlayer(2, Ctx()), CancellationToken.None);
Assert.That(rankMatch, Is.Null, "Different mode shouldn't pair with tk2's waiting viewer.");
}
private static MatchContext Ctx() => new(
SelfDeckCardIds: Enumerable.Range(1, 30).Select(_ => 100_011_010L).ToList(),
ClassId: "1", CharaId: "1", CardMasterName: "card_master_node_10015",
CountryCode: "KOR", UserName: "Player", SleeveId: "3000011",
EmblemId: "701441011", DegreeId: "300003", FieldId: 43, IsOfficial: 0,
BattleType: 11);
}