using System.Collections.Concurrent; using System.Net; using game.PlayerDataStorage; using Newtonsoft.Json; static partial class Tests { sealed class MainLoop : SynchronizationContext { internal readonly BlockingCollection<(SendOrPostCallback,object)> Queue=new(); public override void Post(SendOrPostCallback callback,object state) => Queue.Add((callback,state)); } static Task OnMain(Func body) => Task.Run(()=> { var old=SynchronizationContext.Current; var loop=new MainLoop(); SynchronizationContext.SetSynchronizationContext(loop); try { var task=body(); _=task.ContinueWith(_=>loop.Post(_=>{},null),TaskScheduler.Default); while(!task.IsCompleted) { var (callback,state)=loop.Queue.Take(); callback(state); } task.GetAwaiter().GetResult(); } finally { SynchronizationContext.SetSynchronizationContext(old); } }); sealed class LoadFixture { internal readonly PlayerKVShutdownRegistry Registry; internal readonly int[] Values=new int[31]; internal int Auth,Gets,Sends,Notify,Discard,AuthDisposed,LeaseDisposed; internal bool Fresh=true,Unstarted=true,FailApply,NotifyViolation,AuthDisposeFails,LeaseDisposeFails; internal Action OnNotify; internal Func> GetOverride; internal Func Clock; internal PlayerKVAccountLease Lease; internal PlayerKVGameLoadCoordinator Coordinator; internal LoadFixture(PlayerKVShutdownRegistry registry=null) { Registry=registry ?? new(); var modules=Enumerable.Range(1,31).Select(id=>(IPlayerKVLoadModule)new PlayerKVLoadModule(id, ()=>Values[id-1]=0,_=>{Values[id-1]=id;if(FailApply && id==18) throw new InvalidOperationException();})).ToArray(); var transaction=new PlayerKVGameLoadTransaction(modules,Environment.CurrentManagedThreadId,()=>Discard++); Coordinator=new(transaction,()=>Unstarted,()=>{Notify++;OnNotify?.Invoke();if(NotifyViolation) PlayerKVGameLoadGuard.Reject(PlayerKVGuardPoint.Update);},()=>1, lease=>Lease=lease,()=> {var lease=Lease;Lease=null;return lease?.CloseAsync() ?? Task.CompletedTask;},Registry); } internal PlayerKVGameLoadCapability Capability(Dictionary raw=null,bool realKV=false) { CookieContainer jar=null; return PlayerKVGameLoadCapability.CreateForValidation(Identity(),SchemaSnapshot(),Api,Path.Combine(Root,"load",Guid.NewGuid().ToString("N")), cookies=>{ jar=cookies;return new Handler((r,t)=> { Auth++; Check(r.RequestUri.AbsolutePath=="/account/Login","one auth route"); jar.Add(Api,new Cookie("sid","synthetic")); return Task.FromResult(Json(JsonConvert.SerializeObject(new{state=0,internalId=17,newCreate=Fresh,createTime="2026-01-01T00:00:00Z",svrTime="2026-01-02T00:00:00Z"}))); },()=>{AuthDisposed++;if(AuthDisposeFails)throw new InvalidOperationException("synthetic dispose");});}, cookies=>{Check(ReferenceEquals(cookies,jar),"KV cookie identity");if(realKV) return new HttpClientHandler();return new Handler((r,t)=> {Gets++;Check(r.RequestUri.AbsolutePath=="/playerdata/Get","no mutation route");return GetOverride?.Invoke() ?? Task.FromResult(Json(JsonConvert.SerializeObject(new{data=raw ?? ModernRaw()})));});}, cookies=>{Check(ReferenceEquals(cookies,jar),"lease cookie identity");return new Handler((r,t)=>{Sends++;return Task.FromResult(Json("{}"));},()=>{LeaseDisposed++;if(LeaseDisposeFails)throw new InvalidOperationException("synthetic dispose");});}, delay:Clock); } } static async Task RunCoordinatorTests() { await Run("coordinator-success-reload-revoke-and-one-auth",()=>OnMain(async()=> { var f=new LoadFixture(); var first=await f.Coordinator.LoadAsync("101",f.Capability()); Check(first.State==PlayerKVGameLoadState.LoadedForValidation && first.ModuleCount==31 && f.Coordinator.LoadedForValidation,"controlled success only"); Check(f.Auth==1 && f.Gets==1 && f.Notify==1 && f.Values.All(v=>v>0),"one auth/get and actual transaction"); var oldLease=f.Lease; var second=await f.Coordinator.LoadAsync("102",f.Capability()); Check(second.State==PlayerKVGameLoadState.LoadedForValidation && second.Epoch>first.Epoch && f.Auth==2 && f.AuthDisposed==1 && !oldLease.IsCurrent,"previous account drained before reload"); var close=f.Coordinator.Revoke(); Check(!f.Coordinator.LoadedForValidation && f.Values.All(v=>v==0),"synchronous invalidation and module clearing"); Check(ReferenceEquals(close,f.Coordinator.Revoke()),"same close task"); await close; Check(f.Registry.Status==PlayerKVShutdownStatus.Ready && f.AuthDisposed==2 && f.LeaseDisposed==2,"all ownership drained"); })); await Run("coordinator-schema-apply-and-sideeffect-failures",()=>OnMain(async()=> { foreach(var mode in new[]{0,1,2}) { var f=new LoadFixture{FailApply=mode==1,NotifyViolation=mode==2}; var result=await f.Coordinator.LoadAsync("101",f.Capability(mode==0 ? new Dictionary() : null)); Check(result.Error==(mode==0 ? PlayerKVGameLoadError.Schema : mode==1 ? PlayerKVGameLoadError.Apply : PlayerKVGameLoadError.SideEffect),"stage error preserved"); Check(f.Values.All(v=>v==0) && !f.Coordinator.LoadedForValidation && f.AuthDisposed==1,"failure clears and closes"); Check(f.Notify==(mode==2 ? 1 : 0),"notification cannot precede complete apply"); } var rejected=new LoadFixture(); var real=await rejected.Coordinator.LoadAsync("101",rejected.Capability(realKV:true)); Check(real.State==PlayerKVGameLoadState.Rejected && rejected.Auth==0 && rejected.Gets==0,"all handlers preflight before authentication"); })); await Run("coordinator-cancel-during-notification-and-after-success",()=>OnMain(async()=> { using var cancel=new CancellationTokenSource(); var f=new LoadFixture(); f.OnNotify=()=>cancel.Cancel(); var result=await f.Coordinator.LoadAsync("101",f.Capability(),cancel.Token); Check(result.Error==PlayerKVGameLoadError.Stale && !f.Coordinator.LoadedForValidation && f.Values.All(v=>v==0),"callback revocation clears after transaction unwinds"); using var later=new CancellationTokenSource(); var g=new LoadFixture(); Check((await g.Coordinator.LoadAsync("101",g.Capability(),later.Token)).State==PlayerKVGameLoadState.LoadedForValidation,"ready before later cancel"); later.Cancel(); await g.Coordinator.Revoke(); Check(!g.Coordinator.LoadedForValidation && g.Values.All(v=>v==0),"lifetime remains effective after result"); })); await Run("coordinator-pending-close-cross-instance-and-late-prepare",()=>OnMain(async()=> { var response=new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var started=Signal(); var f=new LoadFixture(); f.GetOverride=()=>{started.TrySetResult(true);return response.Task;}; var deadlines=new List>(); var observedDurations=new List(); f.Clock=(ms,token)=>{var s=Signal();lock(deadlines){deadlines.Add(s);observedDurations.Add(ms);}token.Register(()=>s.TrySetCanceled(token));return s.Task;}; using var cancel=new CancellationTokenSource(); var load=f.Coordinator.LoadAsync("101",f.Capability(),cancel.Token); await started.Task; cancel.Cancel(); // Advance only the live observation clock; transport deliberately ignores cancellation. while(true) { await Task.Yield(); TaskCompletionSource deadline; lock(deadlines) deadline=deadlines.Last(); if(!deadline.Task.IsCompleted) {deadline.TrySetResult(true);break;} } var result=await load; Check(result.Error==PlayerKVGameLoadError.ClosePending && f.Registry.Status==PlayerKVShutdownStatus.Pending && f.AuthDisposed==0,"deadline is not drainage"); var g=new LoadFixture(f.Registry);int ticks=0;g.Clock=(ms,t)=>++ticks==1 ? Task.Delay(Timeout.Infinite,t) : Task.CompletedTask; var blocked=await g.Coordinator.LoadAsync("102",g.Capability()); Check(blocked.Error==PlayerKVGameLoadError.PreviousClosePending && g.Auth==0,"new coordinator cannot bypass pending close"); int observed=0;f.Clock=null; // Existing captured capability owns the quit clock, not a later mutable option. var quit=f.Coordinator.ObserveQuitAsync(); while(!quit.IsCompleted){await Task.Yield();lock(deadlines) deadlines.Last().TrySetResult(true);observed++;} Check(await quit==PlayerKVShutdownStatus.Pending && f.AuthDisposed==0 && observed>=0,"quit observation is not force disposal"); Check(observedDurations.Contains(10000) && observedDurations.Last()==5000,"captured login/quit observations are 10s/5s"); response.TrySetResult(Json(JsonConvert.SerializeObject(new{data=ModernRaw()}))); await f.Coordinator.Revoke(); Check(f.AuthDisposed==1 && f.Notify==0 && f.Values.All(v=>v==0) && !f.Coordinator.LoadedForValidation,"late success cannot apply"); })); await Run("guard-exclusive-across-await-and-all-points",async()=> { Check(PlayerKVGameLoadGuard.TryEnter(out var guard),"first guard"); using(guard) { await Task.Yield(); Check(!PlayerKVGameLoadGuard.TryEnter(out _),"process lease crosses await"); foreach(var point in Enum.GetValues()) Check(PlayerKVGameLoadGuard.Reject(point) && guard.Count(point)==1,"all points counted"); await Throws(()=>{PlayerKVGameLoadGuard.RequireLegacyAllowed(PlayerKVGuardPoint.ReadLocal);return Task.CompletedTask;}); Check(guard.Violations==8,"total violations"); } Check(!PlayerKVGameLoadGuard.Reject(PlayerKVGuardPoint.Update),"PF passthrough outside scope"); }); await Run("coordinator-gate-timeout-cancel-boundary-no-leak",()=>OnMain(async()=> { var f=new LoadFixture{Clock=(ms,t)=>Task.CompletedTask}; Check((await f.Coordinator.LoadAsync("101",f.Capability())).Error==PlayerKVGameLoadError.GateBusy && f.Auth==0,"simultaneous free lock and elapsed deadline rejects"); f.Clock=null;using var cancel=new CancellationTokenSource();cancel.Cancel(); Check((await f.Coordinator.LoadAsync("101",f.Capability(),cancel.Token)).Error==PlayerKVGameLoadError.Stale,"cancelled gate"); Check((await f.Coordinator.LoadAsync("101",f.Capability())).State==PlayerKVGameLoadState.LoadedForValidation,"rejected gate returned permit");await f.Coordinator.Revoke(); var g=new LoadFixture();bool current=false; Check((await g.Coordinator.LoadAsync("102",g.Capability(),identityCurrent:()=>current)).State==PlayerKVGameLoadState.Rejected && g.Notify==0,"changed identity never applies"); })); await Run("coordinator-occupied-gate-bounded-and-reusable",()=>OnMain(async()=> { var started=Signal();var response=new TaskCompletionSource();var f=new LoadFixture(); f.GetOverride=()=>{started.TrySetResult(true);return response.Task;};var first=f.Coordinator.LoadAsync("101",f.Capability());await started.Task; f.Clock=(ms,t)=>Task.CompletedTask; Check((await f.Coordinator.LoadAsync("102",f.Capability())).Error==PlayerKVGameLoadError.GateBusy && f.Auth==1,"busy gate never authenticates next account"); response.SetResult(Json(Newtonsoft.Json.JsonConvert.SerializeObject(new{data=ModernRaw()})));Check((await first).State==PlayerKVGameLoadState.LoadedForValidation,"first untouched");await f.Coordinator.Revoke(); f.Clock=null;f.GetOverride=null;Check((await f.Coordinator.LoadAsync("102",f.Capability())).State==PlayerKVGameLoadState.LoadedForValidation,"permit reusable");await f.Coordinator.Revoke(); })); await Run("coordinator-all-prepare-evidence-under-cleanup-failure",()=>OnMain(async()=> { // Layered mapping probe: 4B1 independently exercises the preparation causes; this invokes the real rejection path for every historical error. var registry=new PlayerKVShutdownRegistry();registry.Register(Task.FromException(new InvalidOperationException("synthetic close"))); var fixture=new LoadFixture(registry); var reject=typeof(PlayerKVGameLoadCoordinator).GetMethod("Reject",System.Reflection.BindingFlags.Instance|System.Reflection.BindingFlags.NonPublic); foreach(var error in Enum.GetValues().Where(e=>e!=PlayerKVLoginPrepareError.None)) { var prepared=new PlayerKVLoginPrepareResult(error,"101",7,httpStatus:401,exceptionType:"SyntheticPrepare",cleanupExceptionType:"SyntheticClose", originalError:error==PlayerKVLoginPrepareError.CloseFailed ? PlayerKVLoginPrepareError.AuthenticationFailed : error); var task=(Task)reject.Invoke(fixture.Coordinator,new object[]{PlayerKVGameLoadError.Prepare,7L,fixture.Capability(),prepared,null}); var result=await task; Check(result.CleanupFailed && result.OriginalError==PlayerKVGameLoadError.Prepare && result.Prepare.Error==error && result.Prepare.OriginalError==prepared.OriginalError,"all historical errors preserved under CloseFailed"); Check(result.Prepare.HttpStatusCode==401 && result.Prepare.ExceptionType=="SyntheticPrepare" && result.Prepare.CleanupExceptionType=="SyntheticClose" && fixture.Auth==0,"stage metadata retained with zero new authentication"); } })); } static async Task IsolatedCleanupFailure(string mode) { await Run("coordinator-cleanup-failure-"+mode,()=>OnMain(async()=> { var f=new LoadFixture{FailApply=mode=="apply",LeaseDisposeFails=mode!="prepare",AuthDisposeFails=mode=="prepare"}; var result=await f.Coordinator.LoadAsync("101",f.Capability(mode=="schema" ? new Dictionary() : null,mode=="prepare")); var expected=mode=="prepare" ? PlayerKVGameLoadError.Prepare : mode=="schema" ? PlayerKVGameLoadError.Schema : PlayerKVGameLoadError.Apply; Check(result.Error==PlayerKVGameLoadError.CloseFailed && result.OriginalError==expected && result.CleanupFailed,"cleanup failure preserves original stage"); Check(result.Prepare!=null && result.ModuleCount==0 && result.IntentCount==0 && !f.Coordinator.LoadedForValidation,"failure carries evidence only"); var next=new LoadFixture(f.Registry);Check((await next.Coordinator.LoadAsync("102",next.Capability())).Error==PlayerKVGameLoadError.PreviousCloseFailed && next.Auth==0,"failure persists across coordinators"); Check(!PlayerKVGameLoadGuard.TryEnter(out _),"failed cleanup cannot release guard"); })); Console.WriteLine($"RESULT isolated mode={mode} failed={failures} assertions={assertions}");return failures==0?0:1; } }