feat(engine-ambient): delete static fallbacks; add MultiInstanceEngineTests

Step 8 (final) of multi-instancing migration. All per-battle statics now
require a BattleAmbient scope — unwrapped writes throw InvalidOperationException
(fail-fast forcing function). MultiInstanceEngineTests proves correctness:
two parallel battles resolve independently, N=4/8/16 stress matches sequential
baseline, GameMgr.GetIns throws without scope.

Migration complete. EngineSessionGate gone. Suite fully green.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
gamer147
2026-06-07 23:19:37 -04:00
parent 9e93a7b198
commit c789d836f1
15 changed files with 449 additions and 131 deletions

View File

@@ -1,40 +1,19 @@
using NUnit.Framework;
namespace SVSim.BattleEngine.Tests
{
// Shared base for every network-emit test fixture (M13 EmitPathReadOracleTests, the
// construction-probe's OnEmit seam test, and any M14+ network fixture to come).
//
// WHY this exists — the global-state leak it closes:
// HeadlessEngineEnv.NewNetworkEmitBattle() mutates two PROCESS globals that no per-mgr
// teardown reverts on its own:
// - Wizard.ToolboxGame.RealTimeNetworkAgent: a non-null agent is injected so the engine's
// NetworkBattleSender ToolboxGame.RealTimeNetworkAgent.* calls resolve (the capture seam).
// - BattleManagerBase.IsForecast: the emit path needs this FALSE, so the harness flips it from
// the EnsureInitialized default of TRUE.
// HeadlessEngineEnv.EnsureInitialized() is _done-guarded — it sets IsForecast=true exactly once
// and then NO-OPs forever after. So once a network-emit fixture leaves IsForecast=false behind,
// nothing restores it: a later SOLO fixture (e.g. RandomDrawOracleTests) that assumes
// IsForecast=true would silently run with IsForecast=false (un-suppressed VFX) and a stale
// injected agent. Currently latent-benign, but a real hygiene gap M14's added network fixtures
// could turn flaky.
// POST-TASK-8 (multi-instancing migration): now empty. The historical hygiene gap this class
// closed (HeadlessEngineEnv.NewNetworkEmitBattle leaving IsForecast=false + a stray injected
// agent visible to a later solo fixture) was a PROCESS-GLOBAL leak via the now-deleted
// BattleManagerBase._isForecastFallback + ToolboxGame._realTimeNetworkAgentFallback statics.
// Both fields are gone: IsForecast/RealTimeNetworkAgent live on the per-test ambient context
// (TestBattleScope's BattleAmbientContext), so scope Dispose drops them. A later fixture's
// new TestBattleScope starts a fresh ctx with IsForecast=true and a null NetworkAgent by
// default — exactly the EnsureInitialized invariant the old TearDown manually restored.
//
// Deriving from this base means NUnit runs the base-class [TearDown] after EVERY test in the
// derived fixture automatically, so the reset can never be forgotten when a new network-emit
// fixture is added — inherit this, don't re-roll a local teardown.
// Kept as a marker base class so derived fixtures don't churn; can be deleted in Task 9.
public abstract class NetworkEmitFixtureBase
{
[TearDown]
public void ResetNetworkEmitGlobals()
{
// Clear the injected agent (the solo oracles don't read it, but clearing it is defensive,
// mirroring RandomDrawOracleTests.ResetRandomDrawGate).
Wizard.ToolboxGame.SetRealTimeNetworkBattle(null);
// Restore the EnsureInitialized default. This is the load-bearing restore: every solo
// oracle and EnsureInitialized assume IsForecast=TRUE, and EnsureInitialized only sets it
// once (guarded), so without restoring it here a solo fixture running after a network-emit
// fixture would see IsForecast=false and un-suppressed VFX.
BattleManagerBase.IsForecast = true;
}
}
}