Commit Graph

356 Commits

Author SHA1 Message Date
gamer147
f589283572 feat(battle-node): BuildMatched consumes MatchContext for player half
selfInfo cosmetics + 30-card selfDeck now read from MatchContext. Opponent
half stays in ScriptedProfiles. DummyCardId / BuildDummyDeck / PlayerMatched
Profile removed. Two new tests lock the deck-idx pairing and cosmetic
flow-through; TypedBodyWireShapeTests + lifecycle tests thread a fixture ctx.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 12:48:04 -04:00
gamer147
01f9bb722a feat(battle-node): thread MatchContext through bridge to BattleSession
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>
2026-06-01 12:44:42 -04:00
gamer147
a0fdb0f3c5 feat(match-context): add IMatchContextBuilder TK2 implementation
Assembles MatchContext from ArenaTwoPickRun + viewer cosmetics + config.
Per-mode interface — future modes (rank/free/open-room/...) add one method
each. DI scoped registration. Four tests cover happy path, no-run, incomplete
draft, default-loadout fallback.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 12:40:26 -04:00
gamer147
89b3d23bde feat(viewer-repo): add LoadForMatchContextAsync for battle-node ctx build
Focused AsNoTracking load with Info.SelectedEmblem/SelectedDegree includes
for the new MatchContextBuilder. Single test locks the include graph.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 12:37:44 -04:00
gamer147
0e8f5427c3 feat(battle-node): add MatchContext record for per-mode player snapshot
Public contract between HTTP-side do_matching controllers (assemble) and
SVSim.BattleNode (consume). First piece of the real-drafted-deck wiring;
nothing references it yet.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 12:35:43 -04:00
gamer147
ef3d7bb82b refactor(battle-node): WireConstants for SIO event names + crypto RNG battle id 2026-06-01 11:53:01 -04:00
gamer147
133346e3e8 refactor(battle-node): SocketIoFrame throws on namespace; typed JSON construction 2026-06-01 11:48:17 -04:00
gamer147
2588388d9d refactor(battle-node): distinct WS auth status codes + named handler delegate 2026-06-01 11:45:50 -04:00
gamer147
a364f539ad refactor(battle-node): tighten Phase setter to private; document sid opacity 2026-06-01 11:41:47 -04:00
gamer147
677b1f1392 feat(battle-node): BattleResult enum for BattleFinish.result wire codes 2026-06-01 11:41:16 -04:00
gamer147
eaf6d7160b refactor(battle-node): dedupe NodeCrypto AES setup into BuildAes helper 2026-06-01 11:36:48 -04:00
gamer147
34c4ca0237 fix(battle-node): NodeCrypto.GenerateKey masks rand source with & 0xF 2026-06-01 11:35:53 -04:00
gamer147
e4fbb155e4 test(battle-node): pump-level tests for async-Task dispatch, CT, Md5 clip 2026-06-01 11:15:10 -04:00
gamer147
21b7ddf6ae test(battle-node): TestWebSocket mock for pump-level unit tests 2026-06-01 11:13:54 -04:00
gamer147
4dd61343aa fix(battle-node): clip SIO ack arg instead of checked-cast throwing on overflow 2026-06-01 11:13:24 -04:00
gamer147
453865ade2 fix(battle-node): thread session CT through every send instead of None 2026-06-01 11:12:26 -04:00
gamer147
8cce667e02 fix(battle-node): await DispatchSocketIo instead of async-void fire-and-forget 2026-06-01 11:11:58 -04:00
gamer147
0764b8646f feat(battle-node): capture session-scoped CT in BattleSession.RunAsync 2026-06-01 11:11:31 -04:00
gamer147
e4691d616b fix(battle-node): emit envelope keys before body keys in MsgEnvelope.ToJson
Client RealTimeNetworkAgent.SetNetworkInfo iterates the synchronize-data
dict in insertion order. The "uri" key, when recognized as Matched, calls
GameMgr.InitializeSelfInfo which sets _selfDeck = null. Any "selfDeck"
processed before "uri" gets wiped; Matching.StartBattleLoad then crashes
on null.Select(...). Pre-refactor ToJson built a Dictionary envelope-first
then appended body keys, so the bug never surfaced. The typed-body rewrite
inverted the order — restoring envelope-first matches the prod wire.

Regression test BuildMatched_KeyOrder_PutsUriBeforeSelfDeckAndSelfInfo
locks the contract.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 10:53:51 -04:00
gamer147
19cc7980d1 test(battle-node): envelope-level wire-shape regression for scripted bodies 2026-06-01 10:40:54 -04:00
gamer147
5ee270eb16 refactor(battle-node): switch MsgEnvelope.Body to IMsgBody, migrate all sites 2026-06-01 10:40:09 -04:00
gamer147
118be92dc5 feat(battle-node): ScriptedProfiles named constants for scripted bodies 2026-06-01 10:35:45 -04:00
gamer147
c7745d8785 feat(battle-node): typed OpponentTurnStart/ResultCodeOnly/BattleFinish/AlivePush bodies 2026-06-01 10:35:18 -04:00
gamer147
97b9b6fe42 feat(battle-node): typed Deal/Swap/Ready bodies + PosIdx 2026-06-01 10:34:44 -04:00
gamer147
78a6fe93fb feat(battle-node): typed BattleStartBody + Self/Oppo info records 2026-06-01 10:34:07 -04:00
gamer147
d9fbb67f0c feat(battle-node): typed MatchedBody + Self/Oppo info records 2026-06-01 10:33:34 -04:00
gamer147
9217de3aa1 feat(battle-node): add IMsgBody marker + RawBody inbound wrapper 2026-06-01 10:32:44 -04:00
gamer147
c279b811ad docs(battle-node): project README + docstrings on hosting/lifecycle
Add a per-project README in SVSim.BattleNode/ that covers:
- Architecture (the six concern folders)
- The connect-handshake sequence verified end-to-end at smoke
- A wire-format-gotchas table for the spec divergences caught during
  v1 (headers vs query for credentials, schemeless node URL with
  /socket.io/ path, required card_master_id, required resultCode=1,
  Matched in response to InitBattle not InitNetwork, EIO3 0x04 prefix
  on binary frames, FromJson conditional-expression number-boxing)
- What the v1 scripted opponent does and what is hardcoded
- A "where to extend" table for v2 work
- The full test layout and cross-references to specs/plans

Fill in XML docs on the public surface that previously had none:
- BattleNodeExtensions.AddBattleNode / UseBattleNode (DI + middleware
  wiring, including the pipeline-order note that auth runs before
  UseWebSockets)
- BattleNodeWebSocketHandler class + HandleAsync (the validation chain)
- BattleSession.ComputeResponses (the lifecycle state machine, with
  the NoStock flag's meaning)
- ScriptedLifecycle class (v1 scope, resultCode injection rule,
  pointer to the "where to extend" section)
- MatchingBridge class (mint-id + register flow)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 08:57:15 -04:00
gamer147
9e8ebd1b2b fix(battle-node): preserve long type on numeric array elements in FromJson
Root cause for the lingering mulligan failure: the inline conditional
expression in MsgEnvelope.ToObject

    JsonValueKind.Number => el.TryGetInt64(out var l) ? l : el.GetDouble(),

unified its branches to the common implicit-convertible type. long→double
is implicit, so both branches collapsed to double and the integer value
silently widened. Inside an array (idxList:[2]), each element came back
as boxed double; OfType<long> in ExtractIdxList then filtered every
entry out, so swapIndices arrived empty and BuildSwapResponse echoed
the unchanged hand — exactly the diff-against-Deal mismatch the client
flagged as "Card swap failed: AbandonCards[2]/DrawCards[]".

Extract a ParseNumber helper that returns object explicitly so each
branch boxes its own runtime type. Also harden ExtractIdxList to accept
any boxed numeric type (long/int/double/decimal/string) so a future
JSON-parser drift can't silently regress this path again.

Two regression tests:
- FromJson_NumericArray_PreservesLongTypeOnEachElement: confirms the
  fix at the JSON-parse layer with a hardcoded "{\"idxList\":[2,3]}".
- Swap_WithIdxListContainingTwo_ProducesHandWithFreshIdxAtPosition1:
  exercises the dispatch end-to-end with a Body holding a real boxed
  long; asserts position 1 of the response hand is the fresh deck idx 4.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 08:40:50 -04:00
gamer147
77fb93f3ea fix(battle-node): real mulligan card replacement + opponent TurnStart push
Two issues caught during v1 smoke at the mulligan / first-turn boundary:

1) BuildSwapResponse ignored the player's idxList and echoed the same
   3-card hand back. The client diffs the new self[] against the Deal
   to compute "drawn cards" — empty diff against the same hand throws
   "Card swap failed: AbandonCards[X]/DrawCards[]". Replace swapped
   idxs with fresh deck idxs (initial hand was 1/2/3, deck has 4..30
   still available). Same hand must flow into Ready since the client
   diffs again there. Move the hand computation into a new helper
   ComputeHandAfterSwap and have ComputeResponses thread it through
   both BuildSwapResponse and BuildReady.

2) The client doesn't transition to the "Opponent's turn…" display
   on its own after sending TurnEnd — it waits for the server to push
   an opponent TurnStart (per prod TK2 capture line 14). Without it
   the UI just sits on the end-of-turn frame. Add a TurnEnd handler
   that pushes a minimal TurnStart{spin} and transitions to a new
   OpponentTurn phase, which IS the documented v1 stopping point.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 08:30:44 -04:00
gamer147
e06d97ef6f fix(battle-node): respond to InitBattle/Loaded, not InitNetwork
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>
2026-06-01 02:08:04 -04:00
gamer147
0b859f1c8e fix(check): merge anonymous resignup viewer into Steam-linked viewer
GameStart already detects the Steam-vs-UDID mismatch produced by
wipe-and-resignup; it now also reclaims the orphan. New
ViewerRepository.MergeAnonymousViewerInto transfers the fresh UDID
from V_new onto V_old in one save (freeing the unique-index slot),
then deletes V_new in a second save. Partial-failure mode is a
benign null-UDID viewer; two rows never contend for the same UDID.
Side benefit: future GetViewerByUdid lookups now short-circuit to
V_old without going through the Steam handler.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 01:59:47 -04:00
gamer147
01b0c64a63 fix(battle-node): inject resultCode=1 into every scripted synchronize push
The client's OnReceived routing drops any synchronize push whose
resultCode != Success(1) — and absent counts as 0(None), which is
also dropped. Our InitNetwork ack and BattleFinish already included
resultCode=1, but the five lifecycle bodies (Matched, BattleStart,
Deal, Swap response, Ready) didn't, so the client silently dropped
every one of them.

Symptom: battle-traffic.ndjson capture showed the client receiving
InitNetwork/Matched/BattleStart, but the UI stayed at the matchmaking
screen until timeout — Matched/BattleStart were dropped at the
routing layer before they ever reached the state machine. Move the
resultCode injection into the shared EnvelopeForPush helper so every
scripted push gets it.

Caught during v1 smoke walkthrough.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 01:55:35 -04:00
gamer147
e7dac31d52 fix(check): emit rewrite_viewer_id when UDID and Steam viewers disagree
Wipe-and-resignup left the client stuck with the blank V_new's id in
Certification.ViewerId. /tool/signup is anonymous, so it can't see the
Steam ticket and creates a fresh anonymous viewer keyed on the new UDID;
the Steam handler on the next request resolves to V_old and serves its
data, but no normal-response hook overwrites Certification.ViewerId.
GameStart now compares the UDID-keyed viewer to the auth-resolved one
and emits rewrite_viewer_id when they differ, which Cute/GameStartCheckTask
writes back into Certification.ViewerId.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 01:49:56 -04:00
gamer147
cc32223d7d fix(battle-node): strip/prepend EIO3 type byte on binary WS frames
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>
2026-06-01 01:48:52 -04:00
gamer147
ccc9b41473 fix(battle-node): header-based WS detection in auth; split unknown-bid vs mismatch logs
Previous fix used Context.WebSockets.IsWebSocketRequest, but that
requires UseWebSockets() to have already run — and UseBattleNode
(which calls UseWebSockets) is registered AFTER UseAuthentication
in Program.cs, so the WS feature isn't installed when auth runs.
Switch to reading the raw Upgrade header, which works regardless
of middleware order.

Also split the WS handler's "Unknown battle/viewer pair" warning
into two distinct cases so we can tell unknown-BattleId from
viewer-id-mismatch (which lets us see whether the bridge stored
the right viewer or the client is encrypting a different id).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 01:17:42 -04:00
gamer147
1252f7bd35 fix(battle-node): read WS credentials from headers; skip Steam auth on WS upgrades
Two issues caught in the real-client smoke:

1) BestHTTP's SocketOptions.AdditionalQueryParams puts BattleId and
   viewerId on HTTP request HEADERS for WebSocket-only transport
   (NOT on the URL query string as the in-battle/transport.md spec
   says). Real clients therefore send them as headers; our handler
   was reading from query and rejecting every connect with "Unknown
   battle/viewer pair: <bid>/<garbage>". Fix: header-first, query-
   fallback (so the integration test still works against TestServer).

2) The Steam auth handler was running on every WS upgrade and
   throwing NotSupportedException on Request.Body.Seek (Kestrel's
   HttpRequestStream doesn't support Seek, and a WS upgrade is GET
   with Content-Length: 0 anyway). It flooded logs and added no
   value — the battle node has its own per-connection credentials.
   Skip auth when IsWebSocketRequest is true.

Spec correction for in-battle/transport.md to follow.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 01:12:21 -04:00
gamer147
5525dbee24 fix(battle-node): node_server_url matches prod wire format (no scheme, with path)
Prod do_matching captures (data_dumps/captures/traffic_prod_tk2_*) send
the node URL as host:port/socket.io/ with no scheme prefix —
e.g. "node06.shadowverse.jp:13560/socket.io/". BestHTTP's SocketManager
expects this exact shape; the leading ws:// we were sending plus the
missing /socket.io/ path was preventing the client from completing the
post-do_matching connect (eventually times out with "connection timed
out").

Update BattleNodeOptions default, Program.cs override, and both
controller and bridge tests to use "localhost:5148/socket.io/".

Discovered during v1 smoke walkthrough.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 01:06:40 -04:00
gamer147
f765d5c7d4 chore(emulated-entrypoint): quiet EFCore info logs in appsettings 2026-06-01 01:02:32 -04:00
gamer147
9776873073 fix(arena-tk2): include card_master_id in do_matching success response
The decompiled client's DoMatchingBase.SettingCardMasterId calls
jsonData["card_master_id"].ToInt() with no Keys.Contains guard when
matching_state ∈ {3004, 3007, 3011}. Omitting the field crashes the
client with KeyNotFoundException at Cute.NetworkManager+Connect.

Add CardMasterId to DoMatchingResponseDto with a default value of 1
(matching the /load/index response and prod captures). Extend the
controller test to assert the field is present.

Caught during the v1 smoke walk-through; full client log line:
  [Error: Unity Log] KeyNotFoundException: The given key was not
  present in the dictionary.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 00:52:57 -04:00
gamer147
b1397e3a3e refactor(repositories): move static caches into IMemoryCache, enable within-fixture parallelism
BattlePassRepository._curveCache and MissionCatalogRepository._maxLevelCache
were private-static fields populated lazily on first read from whatever
DbContext happened to be in scope. In production "one DbContext lineage
per process" makes that fine. Under parallel test execution each
SVSimTestFactory owns its own SQLite :memory: DB, so the first reader's
DB (often empty, in tests that don't seed BP) poisoned the cache for
concurrent readers from a seeded DB — assertions like "BP level info
must be present after seeding" failed because the process-static cache
returned an empty list populated by the other test's empty DB.

The first patch attempted a `BypassCacheForTests` static flag, which is
exactly the kind of test-only seam that rots the production code: future
caches get the same flag, repos accumulate hidden knobs, and the
underlying invariant ("a cache populated from arbitrary scope serves
arbitrary scope") goes unaddressed.

Instead, move both caches into the DI-registered IMemoryCache.
AddMemoryCache() registers it as singleton-per-service-provider:
production has one provider → one IMemoryCache → identical caching
semantics to before. Each WebApplicationFactory builds its own
provider → its own IMemoryCache → cache is naturally scoped per fixture,
no cross-test bleed possible.

The ResetLevelCurveCache() method and its three call sites
(SVSimTestFactory.SeedGlobalsAsync, BattlePassServiceTests,
LoadControllerTests) are deleted — a fresh factory owns a fresh empty
cache, no manual invalidation needed.

With this and the previous StoryService fixture-instance fix in place,
ParallelScope.All works: 776/776 in 57s wall clock (down from 59s on
Fixtures, 2m13s pre-parallelism).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 00:41:09 -04:00
gamer147
66c456c1c8 test(story-service): per-test fixture instance + unique InMemoryDb name
NUnit's default FixtureLifeCycle is SingleInstance — every test in a
class shares one fixture instance, so [SetUp]-initialised fields like
_master / _viewer / _service are reset on every test against the same
object. Under serial execution that's fine; under parallel execution
concurrent SetUps wipe each other's Mock setups and the service code
NREs trying to dereference unconfigured stubs.

Compounding it, NewInMemoryDb was being called with nameof(SetUp) which
is the literal string "SetUp", so every test in the fixture also shared
the same EF InMemory database (the provider keys stores by name).

Two fixes:
- [FixtureLifeCycle(LifeCycle.InstancePerTestCase)] on StoryServiceTests
  so each test gets its own instance with its own Mocks.
- Suffix the InMemoryDb name with a Guid so concurrent callers never
  share a store.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 00:40:51 -04:00
gamer147
31f26655ba test(pack-controller): derive expected active-pack count from seed at runtime
The full-catalog regression test hardcoded "35 active packs as of
2026-05-23" but the controller filters by DateTime.UtcNow against each
pack's commence/complete dates. When two packs (99047, 80047) crossed
their complete_date of 2026-06-01 01:59:59 UTC, the test started
failing with Expected: 35 / But was: 33 — which had been masked all
along by NUnit's trx serializer OOMing on a different test.

The hardcoded count conflated three things that happened to be equal
on the day the test was written: packs in the seed file, packs active
right now, and 35. The test's real intent (per its class docstring) is
"every pack the importer ingests round-trips through /pack/info";
pinning the clock with TimeProvider would solve today's drift but
re-break the moment someone regenerates the seed or retires a pack.

Expected count now derives from the seed file at test time, filtered
by the same predicate the controller uses (PackRepository
.GetActivePacks: IsEnabled && commence <= now <= complete) via the
shared ImporterBase.ParseWireDateTime parser so any date-string quirk
parses identically on both sides. Spot-check on pack 99047 swapped for
"any pack with non-default pack_category" — same schema-fidelity
coverage (non-zero category survives JSON round trip) without pinning
to an id that rotates.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 00:23:15 -04:00
gamer147
7914bab84e test(unit-tests): silence captured stdout in Testing env
The unit-test suite was spending most of its wall clock writing logs.
NUnit captures stdout per test and embeds it in the trx; with HttpLogging
emitting full request/response per controller call, EF Core SQL at
Information level, and ReferenceDataImporter banners running ~500x
(once per factory construction), the trx grew to 3.2 GB and the NUnit
result-XML serializer OOMed in StringBuilder.ToString() — which the
runner reported as one mysteriously failed test, masking a real
date-dependent failure underneath.

Three sources silenced under environment "Testing":
- appsettings.Testing.json drops Default + Microsoft.AspNetCore +
  HttpLoggingMiddleware + EntityFrameworkCore to Warning.
- Program.cs skips app.UseHttpLogging() entirely (avoids the
  middleware overhead, not just the log emission).
- ReferenceDataImporter takes optional TextWriters; the test factory
  passes TextWriter.Null. Per-importer helpers become instance methods
  so they can use the injected writer.

Result on a fresh run with ParallelScope.Fixtures already in place:
- Test duration: 1m46s -> 59s
- Wall clock: 2m23s -> 1m00s
- trx size: 3.2 GB -> 1.7 MB

The previously-masked date-dependent failure (PackControllerFullCatalog
.Info_returns_full_35_pack_catalog_from_production_seed asserting 35
active packs as of 2026-05-23 against a live clock) is now visible and
can be addressed separately.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 00:13:42 -04:00
gamer147
d093d872ae test(unit-tests): parallelize at the fixture level
NUnit's default ParallelScope is Self (serial). With ~736 tests each
constructing its own SVSimTestFactory (full ASP.NET host + SQLite :memory:
+ ReferenceDataImporter seeding 7270 rows from CSVs), the suite was
running ~2m13s serial. ParallelScope.Fixtures drops it to ~1m46s — a
~20% wall-clock reduction with zero new failures.

Stayed at Fixtures rather than All because ParallelScope.All exposes
the process-static BattlePassRepository._curveCache (and likely other
similar caches) to races inside heavy-globals fixtures (LoadController,
PackControllerFullCatalog, StoryService — all consistent failures
under All, flaky 3-7 fails across runs). Within-fixture parallelism
is blocked on cleaning those up first.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 00:06:47 -04:00
gamer147
905fdc780a test(battle-node): end-to-end flow test through Ready via WebApplicationFactory
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>
2026-05-31 23:37:31 -04:00
gamer147
ff51c33b6c feat(arena-tk2): do_matching mints battle via IMatchingBridge, returns 3004 2026-05-31 22:53:20 -04:00
gamer147
88ed8254af feat(emulated-entrypoint): wire AddBattleNode + UseBattleNode into the web host 2026-05-31 22:49:31 -04:00
gamer147
1dd6a70e8d feat(battle-node): WebSocket endpoint at /socket.io/ + DI extension methods 2026-05-31 22:34:54 -04:00
gamer147
f19da481c3 fix(battle-node): MatchingBridge avoids Math.Abs(int.MinValue) overflow
Cast GetHashCode() result to long before Math.Abs to prevent OverflowException
on the ~1-in-4B case where GetHashCode returns int.MinValue. Adds a regression
test pinning the 12-digit decimal format end-to-end.

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