using NUnit.Framework; using ProjectM.Server; using Unity.Entities; using Unity.NetCode; namespace ProjectM.Tests { /// /// Regression pin for the RPC-leak class the 2026-08-06 audit reproduced live: every RPC receiver in this /// project gates itself behind RequireForUpdate on a scene-baked singleton, so in a scene missing that /// singleton the receiver never runs and the request entity is never destroyed. Netcode's own stale-RPC /// warning Consume()s but never destroys, and is compiled out of player builds — so the entities accumulated /// forever, worst in a shipped build. /// /// is the terminal drain. These tests pin the two halves of its contract: /// an aged UNCONSUMED request is destroyed, and a CONSUMED one is left alone for its owning receiver. /// public class StaleRpcReaperSystemTests { [Test] public void Unconsumed_Request_Is_Destroyed_Once_It_Has_Aged() { var (world, group) = TestWorld.Make("ReaperAged", tick: 100, server: true); using (world) { var em = world.EntityManager; var e = em.CreateEntity(); // Age 2 == "survived a full frame unhandled" (the reaper's k_MaxAgeFrames). em.AddComponentData(e, new ReceiveRpcCommandRequest { SourceConnection = Entity.Null, Age = 2 }); group.Update(); Assert.IsFalse(em.Exists(e), "an unconsumed RPC request that outlived its receiving frame must be reaped, not leaked forever."); } } [Test] public void Fresh_Request_Survives_So_A_Late_Receiver_Still_Sees_It() { var (world, group) = TestWorld.Make("ReaperFresh", tick: 100, server: true); using (world) { var em = world.EntityManager; var e = em.CreateEntity(); em.AddComponentData(e, new ReceiveRpcCommandRequest { SourceConnection = Entity.Null, Age = 0 }); group.Update(); Assert.IsTrue(em.Exists(e), "a request that arrived this frame must NOT be reaped — a receiver ordered later still owns it."); } } [Test] public void Consumed_Request_Is_Left_To_Its_Receiver() { var (world, group) = TestWorld.Make("ReaperConsumed", tick: 100, server: true); using (world) { var em = world.EntityManager; var e = em.CreateEntity(); var recv = new ReceiveRpcCommandRequest { SourceConnection = Entity.Null, Age = 250 }; recv.Consume(); em.AddComponentData(e, recv); group.Update(); Assert.IsTrue(em.Exists(e), "a CONSUMED request belongs to the receiver that consumed it; the reaper must not race it."); } } } }