using System.Reflection; using game.PlayerDataStorage; using Newtonsoft.Json; static partial class Tests { // Inspect identity only; all loading, storage operations and shutdown use real production code. static PlayerKVLoginContext LoadContext(LoadFixture fixture) => (PlayerKVLoginContext) typeof(PlayerKVGameLoadCoordinator).GetField("context",BindingFlags.Instance|BindingFlags.NonPublic).GetValue(fixture.Coordinator); static async Task RunGameHandoffTests() { await Run("M2-CP1-original-handle-KVMgr-local-write-and-synchronous-revoke",()=>OnMain(async()=> { var f=new LoadFixture(); Check(!f.Coordinator.TryGetCurrentGameHandle(1,out _),"no handle before load"); f.OnNotify=()=>Check(!f.Coordinator.TryGetCurrentGameHandle(1,out _),"notify cannot observe published handle"); try { var result=await f.Coordinator.LoadForGameAsync("101",f.Capability()); Check(result.State==PlayerKVGameLoadState.GameLoaded && result.ModuleCount==31,"game state after full apply"); Check(f.Coordinator.TryGetCurrentGameHandle(result.Epoch,out var handle),"current game handle"); var context=LoadContext(f); Check(context.TryAcquireGameHandle(result.Epoch,out var original) && ReferenceEquals(handle,original),"same prepared object, not another owner"); Check(f.Coordinator.TryGetCurrentGameHandle(result.Epoch,out var again) && ReferenceEquals(handle,again),"stable object on repeated access"); Check(!context.TryAcquireGameHandle(result.Epoch+1,out _) && !f.Coordinator.TryGetCurrentGameHandle(result.Epoch+1,out _),"wrong epoch denied"); Check(KVMgr.WriteKV(handle,"m2-handoff","local-only",false).Accepted,"released guard permits synthetic local edit"); Check(KVMgr.ReadKV(handle,"m2-handoff").Value=="local-only","read same owner cache"); Check(f.Auth==1 && f.Gets==1 && f.Notify==1,"handoff does not authenticate or fetch again"); var closing=f.Coordinator.Revoke(); Check(!f.Coordinator.TryGetCurrentGameHandle(result.Epoch,out _) && !context.TryAcquireGameHandle(result.Epoch,out _),"synchronous unpublication before awaiting drain"); Check(!KVMgr.WriteKV(handle,"m2-handoff","stale",false).Accepted && !handle.ReadLocalSnapshot().Success,"captured old object loses authority immediately"); await closing; Check(f.Values.All(v=>v==0) && f.AuthDisposed==1 && f.LeaseDisposed==1,"modules and ownership drained"); } finally { await f.Coordinator.Revoke(); } })); await Run("M2-CP1-failures-never-publish-and-release-after-cleanup",()=>OnMain(async()=> { foreach(var mode in new[]{"schema","apply","notify-throw","notify-violation","notify-revoke","notify-cancel","identity"}) { using var cancel=new CancellationTokenSource(); var f=new LoadFixture{FailApply=mode=="apply",NotifyViolation=mode=="notify-violation"}; f.OnNotify=()=>{if(mode=="notify-throw")throw new InvalidOperationException("notify failure");if(mode=="notify-revoke")_=f.Coordinator.Revoke();if(mode=="notify-cancel")cancel.Cancel();}; var result=await f.Coordinator.LoadForGameAsync("101",f.Capability(mode=="schema" ? new Dictionary() : null),cancel.Token,()=>mode!="identity"); Check(result.State==PlayerKVGameLoadState.Rejected && result.ModuleCount==0 && result.IntentCount==0,"failure cannot claim applied game: "+mode); Check(!f.Coordinator.TryGetCurrentGameHandle(result.Epoch,out _) && !f.Coordinator.LoadedForValidation,"no alternate success state: "+mode); Check(f.Values.All(v=>v==0),"failure clears containers: "+mode); await f.Coordinator.Revoke(); Check(PlayerKVGameLoadGuard.TryEnter(out var guard),"cleanup releases guard: "+mode);guard.Dispose(); } })); await Run("M2-CP1-same-account-new-epoch-and-validation-isolation",()=>OnMain(async()=> { var f=new LoadFixture(); try { var first=await f.Coordinator.LoadForGameAsync("101",f.Capability()); Check(f.Coordinator.TryGetCurrentGameHandle(first.Epoch,out var old),"first handle"); var second=await f.Coordinator.LoadForGameAsync("101",f.Capability()); Check(second.Epoch>first.Epoch && f.Coordinator.TryGetCurrentGameHandle(second.Epoch,out _),"same account new epoch"); f.Coordinator.TryGetCurrentGameHandle(second.Epoch,out var current); Check(!ReferenceEquals(old,current) && old.OwnerInstanceId!=current.OwnerInstanceId,"new load has distinct fixed owner"); Check(!f.Coordinator.TryGetCurrentGameHandle(first.Epoch,out _) && !old.Update("old","no",false).Accepted,"old callback cannot reuse new account"); var validation=await f.Coordinator.LoadAsync("101",f.Capability()); Check(validation.State==PlayerKVGameLoadState.LoadedForValidation && f.Coordinator.LoadedForValidation,"legacy validation state preserved"); Check(!f.Coordinator.TryGetCurrentGameHandle(validation.Epoch,out _) && !f.Coordinator.TryGetCurrentGameHandle(second.Epoch,out _),"validation never exposes a game handle"); Check(KVMgr.ReadKV(current,"old").Error==PlayerKVKeyReadError.LoadGuardActive,"validation still holds guard"); } finally { await f.Coordinator.Revoke(); } })); await Run("M2-CP1-context-unprepared-and-reentrant-close-no-monitor-deadlock",()=>OnMain(async()=> { var f=new LoadFixture();bool closeOnCheck=false;Task closing=null;PlayerKVLoginContext context=null; var cap=f.Capability(); context=cap.CreateContext("101",7,_=> { if(closeOnCheck) { closeOnCheck=false; // Only wait for synchronous Close admission, never for async drainage. var admission=Task.Run(()=>{closing=context.CloseAsync();}); Check(admission.Wait(TimeSpan.FromSeconds(3)),"identity callback outside context monitor"); } return true; }); try { Check(!context.TryAcquireGameHandle(7,out _),"unprepared context cannot issue"); Check((await context.PrepareAsync()).Prepared,"prepared context"); Check(context.TryAcquireGameHandle(7,out var original),"prepared context original"); closeOnCheck=true; Check(!context.TryAcquireGameHandle(7,out _),"reentrant close invalidates issuance"); Check(!original.Update("closed","no",false).Accepted,"captured original revoked"); await closing; } finally { await context.CloseAsync();cap.CloseUnused(); } })); await Run("M2-CP1-revoke-at-publication-and-during-getter",()=>OnMain(async()=> { foreach(var when in new[]{"publication","getter"}) { var f=new LoadFixture();bool armed=false;int afterNotify=0;Task closing=null; bool Identity() { if((when=="publication" && f.Notify>0 && ++afterNotify==3) || (when=="getter" && armed)) { armed=false; var admission=Task.Run(()=>{closing=f.Coordinator.Revoke();}); Check(admission.Wait(TimeSpan.FromSeconds(3)),"external identity callback outside coordinator publication monitor"); } return true; } try { var result=await f.Coordinator.LoadForGameAsync("101",f.Capability(),identityCurrent:Identity); if(when=="getter") { Check(result.State==PlayerKVGameLoadState.GameLoaded,"success before getter revocation");armed=true; Check(!f.Coordinator.TryGetCurrentGameHandle(result.Epoch,out _),"getter must recheck after callback"); } else Check(result.State==PlayerKVGameLoadState.Rejected,"revocation before publication cannot return success"); Check(closing!=null,"race barrier actually reached");await closing; Check(!f.Coordinator.TryGetCurrentGameHandle(result.Epoch,out _) && f.Values.All(v=>v==0),"revoked state stays unpublished"); } finally { await f.Coordinator.Revoke(); } } })); await Run("M2-CP1-background-revoke-during-prepare-discards-late-response",()=>OnMain(async()=> { var started=Signal();var response=new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);var f=new LoadFixture(); f.GetOverride=()=>{started.TrySetResult(true);return response.Task;}; var load=f.Coordinator.LoadForGameAsync("101",f.Capability());await started.Task; Task close=null;await Task.Run(()=>{close=f.Coordinator.Revoke();}); Check(!f.Coordinator.TryGetCurrentGameHandle(1,out _),"no handle while close drains late transport"); response.SetResult(Json(JsonConvert.SerializeObject(new{data=ModernRaw()}))); var result=await load;await close; Check(result.State==PlayerKVGameLoadState.Rejected && f.Notify==0 && f.Values.All(v=>v==0),"late prepare cannot publish or apply"); })); } }