Skip to content

feat(tests): standalone .NET dispatch test (closes #11)#13

Open
Taure wants to merge 4 commits into
mainfrom
feat/asobicore-dotnet-extraction
Open

feat(tests): standalone .NET dispatch test (closes #11)#13
Taure wants to merge 4 commits into
mainfrom
feat/asobicore-dotnet-extraction

Conversation

@Taure
Copy link
Copy Markdown
Contributor

@Taure Taure commented May 2, 2026

Why

smoke.yml uses game-ci/unity-test-runner@v4 which needs UNITY_LICENSE / UNITY_EMAIL / UNITY_PASSWORD secrets to compile + run anything (#11). Those secrets aren't provisioned, so neither the existing PlayMode smoke nor the 32-fixture protocol dispatch test added in #12 can act as a required check today.

This is the same problem the Unreal SDK had. We solved it there by extracting the engine-agnostic C++ into a CMake static lib + doctest exe (Source/AsobiCore/) that runs on stock ubuntu-24.04. This PR is the C# equivalent for asobi-unity.

What

A standalone dotnet test project that exercises the same dispatch logic Unity runs, with zero Unity dependencies.

Refactor (decision: extract pure protocol code)

Runtime/WebSocket/AsobiDispatcher.cs — new pure-C# base class that owns:

  • All On* event declarations (35 events)
  • HandleMessage(raw) — the dispatch switch
  • ProtocolEnvelope.Parse(raw) — a tiny zero-dep parser that pulls just type and cid from the envelope (replaces the Unity-only JsonUtility.FromJson<WsMessage> in the dispatch path)

AsobiRealtime now : AsobiDispatcher — only retains the Unity-aware concerns (ClientWebSocket, JsonUtility payload serialization, async send/receive). Cid resolution stays on the subclass via an OnPendingResponse virtual hook.

No public API changes — the events stay where consumers see them on AsobiRealtime (inherited).

Standalone .NET test project

Tests/AsobiCore.NET/:

  • Asobi.Core.Tests.csproj — net8.0, NUnit 4 (matches the existing Unity test asmdef which uses NUnit attributes)
  • <Compile Include="..\..\Runtime\WebSocket\AsobiDispatcher.cs" Link="..."/> — shared source, no copies
  • <None Include="..\..\Tests\Runtime\Resources\Fixtures\*.json" CopyToOutputDirectory="PreserveNewest" /> — same 32 fixtures Unity loads via Resources.LoadAll<TextAsset>, here loaded from AppContext.BaseDirectory/Fixtures/
  • DispatcherTests.cs — port of Tests/Runtime/DispatchTests.cs (from feat(tests): protocol dispatch test against canonical fixtures #12), targeting AsobiDispatcher directly

Tests/AsobiCore.NET/ also has an asmdef gated on a never-set defineConstraints so Unity's predefined assembly never picks up DispatcherTests.cs.

What it tests

  • 32 fixture cases (one [Test] per fixture via [TestCaseSource])
  • EveryFixtureHasExpectedMapping / EveryExpectedHasFixture — pin the bijection between fixture set and dispatch switch
  • MatchmakerMatchedAliasesMatchMatched — pins the matchmaker.matched -> match.matched historical alias
Total tests: 35
     Passed: 35
 Total time: 0.79 Seconds

CI

.github/workflows/dispatch.yml — new job on ubuntu-24.04, just setup-dotnet + dotnet test. No secrets. This is the new required check for protocol regressions. The existing smoke.yml stays so the PlayMode test runs once #11's secrets land.

SmokeTest.cs fixes

The file shipped with #8 had compile errors (called .match_id on string event payloads, used non-existent SendMatchInput / Disconnect APIs) that didn't show up because the Unity job never compiled them. Fixed in a separate commit:

  • Read match_id via JsonHelper.ExtractJsonField on the raw event JSON
  • SendMatchInput -> SendMatchInputAsync
  • Disconnect -> DisconnectAsync

Coordination with #12

Per repo convention, kept #12's Unity-side test alongside this one (option (a) in the brief). This PR cherry-picks #12's commit so the dispatcher refactor can build on the events #12 introduced (OnHeartbeat, OnMatchFinished, etc.). When either PR merges first, the other rebases cleanly — git detects the identical commit. The .NET test in this PR becomes the required check; the PlayMode test stays for when secrets land.

Test plan

  • dotnet test Tests/AsobiCore.NET/Asobi.Core.Tests.csproj — 35/35 green locally
  • CI dispatch.yml passes on ubuntu-24.04 with no secrets
  • Verify Unity asmdef still compiles (manual — no license to run locally; review the diff for import breakage)

Closes #11.

Do not merge — leave for morning review.

Taure added 4 commits May 2, 2026 09:13
Per-SDK protocol dispatch unit test: feeds every canonical
server-emitted message envelope through AsobiRealtime.HandleMessage and
asserts the matching SDK event fires. Vendors the 32-fixture corpus from
asobi/priv/protocol/fixtures into Tests/Runtime/Resources/Fixtures.

Audit findings against the canonical corpus turned up six dispatch
cases that were silently falling through to the generic OnMatchEvent /
OnWorldEvent buckets, plus session.heartbeat with no surfaced event.
Added explicit handlers and events:

  match.finished           -> OnMatchFinished
  match.matchmaker_expired -> OnMatchmakerExpired
  match.matchmaker_failed  -> OnMatchmakerFailed
  world.finished           -> OnWorldFinished
  world.list               -> OnWorldList
  world.phase_changed      -> OnWorldPhaseChanged
  session.heartbeat        -> OnHeartbeat

The defensive matchmaker.matched alias is kept as a fall-through to
OnMatchmakerMatched with a TODO-deprecate note; removing without a
deprecation cycle would break consumers that listened for it during the
historical drift period. The alias is pinned by a dedicated test.

Made HandleMessage internal and added a parameterless test-only ctor to
allow exercising dispatch without a live WebSocket. Asobi.Tests already
references Asobi via its asmdef; InternalsVisibleTo is wired through a
new Runtime/AssemblyInfo.cs.

The test also asserts the bidirectional invariant: every fixture has
an Expected mapping and every Expected entry has a fixture, so future
drift in either direction fails CI.
Pulls all events + the HandleMessage switch out of AsobiRealtime into a
new pure-C# AsobiDispatcher base class (no UnityEngine deps). Adds a
zero-dep envelope parser (ProtocolEnvelope) so dispatch no longer needs
JsonUtility.

AsobiRealtime now inherits AsobiDispatcher and only retains the WebSocket
+ JsonUtility serialization concerns. Cid resolution stays on the
subclass via an OnPendingResponse override hook.

Enables a standalone .NET dispatch test that doesn't need Unity.
Adds Tests/AsobiCore.NET/ — a stock dotnet 8 NUnit project that compiles
the shared Runtime/WebSocket/AsobiDispatcher.cs source (via csproj
<Compile Include=...> with no copies) and runs the same 32-fixture
dispatch suite + the matchmaker.matched alias test from the existing
Unity-side DispatchTests.

Fixtures live under Tests/Runtime/Resources/Fixtures/ and are referenced
via <None Include> with CopyToOutputDirectory so the test reads them
from AppContext.BaseDirectory at run time. Same source of truth as the
Unity asmdef.

Adds .github/workflows/dispatch.yml running 'dotnet test' on stock
ubuntu-24.04 with no Unity license required. The existing Smoke
workflow stays so the PlayMode test still runs once UNITY_LICENSE
secrets land (#11).

Unity sees Tests/AsobiCore.NET/ via an asmdef gated on a never-set
defineConstraint so the .NET test code is never picked up by Unity's
predefined assembly.
The PlayMode SmokeTest predates the typed-event work. It assumed:
- OnMatchmakerMatched / OnMatchState carry typed payload structs (they
  carry raw JSON strings)
- SendMatchInput / Disconnect exist (the actual API is
  SendMatchInputAsync / DisconnectAsync)

Switches the handlers to read fields via JsonHelper.ExtractJsonField
(internal, visible to the test asmdef via the existing IVT) and uses
the actual async API names so the file compiles.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

smoke CI: add UNITY_LICENSE / UNITY_EMAIL / UNITY_PASSWORD secrets

1 participant