From e6a561b30f9b722c9c93b8d0c2c7991573288758 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Sat, 6 Jun 2026 13:03:07 -0400 Subject: [PATCH] =?UTF-8?q?test(battle-engine=20M13):=20shared=20NetworkEm?= =?UTF-8?q?itFixtureBase=20teardown=20=E2=80=94=20close=20IsForecast/agent?= =?UTF-8?q?=20global=20leak?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 --- .../EmitPathReadOracleTests.cs | 18 ++------- .../NetworkEmitFixtureBase.cs | 40 +++++++++++++++++++ .../NetworkMgrConstructionProbeTests.cs | 2 +- 3 files changed, 45 insertions(+), 15 deletions(-) create mode 100644 SVSim.BattleEngine.Tests/NetworkEmitFixtureBase.cs diff --git a/SVSim.BattleEngine.Tests/EmitPathReadOracleTests.cs b/SVSim.BattleEngine.Tests/EmitPathReadOracleTests.cs index 7bf5b05..636f1ed 100644 --- a/SVSim.BattleEngine.Tests/EmitPathReadOracleTests.cs +++ b/SVSim.BattleEngine.Tests/EmitPathReadOracleTests.cs @@ -10,21 +10,11 @@ namespace SVSim.BattleEngine.Tests // without crashing, while the committed state still matches the M3 direct-ActionProcessor oracle. // Liveness only (E4); structural frame decoding + the RNG rand-list (M14) are deferred. [TestFixture] - public class EmitPathReadOracleTests + public class EmitPathReadOracleTests : NetworkEmitFixtureBase { - // Reset the process globals this fixture mutates so no later fixture inherits them: - // - ToolboxGame.RealTimeNetworkAgent: the injected agent (the solo oracles don't read it, but - // clearing it is defensive, mirroring RandomDrawOracleTests.ResetRandomDrawGate). - // - BattleManagerBase.IsForecast: the network emit harness sets this FALSE (the emit path needs it); - // every solo oracle and EnsureInitialized assume it TRUE. EnsureInitialized only sets it once - // (guarded), so without restoring it here a solo fixture running after this one would see - // IsForecast=false and un-suppressed VFX. Restore the EnsureInitialized default. - [TearDown] - public void ResetGlobals() - { - Wizard.ToolboxGame.SetRealTimeNetworkBattle(null); - BattleManagerBase.IsForecast = true; - } + // The process-global reset (IsForecast=true + clear injected agent) now lives in the shared + // NetworkEmitFixtureBase.ResetNetworkEmitGlobals [TearDown], inherited here — see that file + // for why the leak matters. [Test] public void M3_spell_driven_via_OperateMgr_reaches_emit_without_crashing() diff --git a/SVSim.BattleEngine.Tests/NetworkEmitFixtureBase.cs b/SVSim.BattleEngine.Tests/NetworkEmitFixtureBase.cs new file mode 100644 index 0000000..ba7e70f --- /dev/null +++ b/SVSim.BattleEngine.Tests/NetworkEmitFixtureBase.cs @@ -0,0 +1,40 @@ +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. + // + // 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. + 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; + } + } +} diff --git a/SVSim.BattleEngine.Tests/NetworkMgrConstructionProbeTests.cs b/SVSim.BattleEngine.Tests/NetworkMgrConstructionProbeTests.cs index 28873e8..a1262ec 100644 --- a/SVSim.BattleEngine.Tests/NetworkMgrConstructionProbeTests.cs +++ b/SVSim.BattleEngine.Tests/NetworkMgrConstructionProbeTests.cs @@ -9,7 +9,7 @@ namespace SVSim.BattleEngine.Tests // _battleCamera, _backGround) + RegisterActionManager + OperateReceive — the largest new shim // surface since M5's prefab path. Isolate "ctor runs" before any play is driven. [TestFixture] - public class NetworkMgrConstructionProbeTests + public class NetworkMgrConstructionProbeTests : NetworkEmitFixtureBase { [Test] public void HeadlessNetworkBattleMgr_constructs_headless()