Single-client end-to-end Bot lifecycle: InitNetwork → ack, InitBattle →
ack (no Matched), Loaded → silent, Swap → SwapResponse + Ready,
two TurnEnd cycles each producing a single Judge frame back to sender,
Retire → BattleFinish. Pending battle evicted at session start.
Closes Phase 3 — battle-node v2's three-phase migration (Scripted → PvP →
Bot) is now complete. Test budget: 884 → 931 (+47 across Phase 3).
Next: matching-queue API rewrite + real rank progression, as separate
specs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Four end-to-end tests against two parallel RawSocketIoTestClients:
handshake to AfterReady on both sides with per-perspective Matched;
TurnEnd broadcast to both sides + Judge; A's PlayActions forwarded to
B; Retire flipped to Lose-for-sender, Win-for-other; A's abrupt WS
close cascades to BattleFinish(Win) for B with PendingBattle eviction;
waiting-room timeout closes the first arriver's WS (fallback long-wait
path — the 60s default is left in place; TestServer-side WS close is
observed via ReceiveAsync returning Close or throwing).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
End-to-end through the real WS pump: after Ready, the test sends two
consecutive TurnEnd msgs and asserts the server pushes
TurnStart+TurnEnd for each. Exercises OutboundSequencer's playSeq
assignment across multiple cycles.
Seeds a viewer + completed TK2 run, drives the WS handshake to Matched, and
asserts every cardId in selfDeck matches the run's SelectedCardIdsJson. Read
from RawBody (codec's wire-form deserialization) — not from MatchedBody —
since the test client gets the JSON-roundtripped envelope.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
IMatchingBridge.RegisterPendingBattle now takes a MatchContext; PendingBattle
carries it; BattleSession stores it. ArenaTwoPickBattleController builds ctx
from IMatchContextBuilder. ScriptedLifecycle still uses ScriptedProfiles for
the player half — Tasks 5/6 migrate the lifecycle.
Existing tests updated: MatchingBridgeTests, BattleNodeFlowTests,
InMemoryBattleSessionStoreTests, BattleSessionDispatchTests, BattleSession
PumpTests, ArenaTwoPickBattleControllerTests (which now seeds a TK2 run +
adds a no-active-run 400 case).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>
Engine.IO v3 frames over WebSocket prepend the packet-type byte (0x04
for Message) to BINARY frames, the binary analog of the leading digit
on text frames. The real client honors this and our session was
treating the entire binary frame as the Socket.IO attachment payload —
the msgpack decoder saw 0x04 as a positive fixint and failed
deserialization on every inbound msg event.
Symmetric fix: strip 0x04 from inbound binary frames in
BattleSession.RunAsync, prepend 0x04 to outbound binary frames in
EncodeAndSendAsync. RawSocketIoTestClient gets the same on both
directions so the integration test still exercises the same wire
shape as a real client.
Caught during v1 smoke walkthrough, after the WS upgrade started
succeeding (101 Switching Protocols).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Boots SVSimTestFactory (in-memory SQLite + reference-data CSV import),
mints a battle via IMatchingBridge, opens a raw Socket.IO v2 client
against the in-process TestServer, drives InitNetwork → Loaded → Swap,
and asserts the right scripted frames come back in order.
Verifies the full transport stack end-to-end: EIO3+SIO2 framing,
encryptForNode codec, MsgPayloadCodec roundtrip, InboundTracker
pubSeq dedup + ack echo, OutboundSequencer playSeq assignment, and
ScriptedLifecycle's Path-A frame builders.
Note: RawSocketIoTestClient.DisposeAsync skips the graceful CloseAsync
handshake — TestServer's in-process WebSocket implementation can hang
on it. Abrupt Dispose is fine: the server's ReceiveAsync throws
WebSocketException, BattleSession.RunAsync returns, and the handler
completes.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>