using System.IO.Pipes; using System.Text.Json; using XFEEDToolBox.Core.Models; using XFEEDToolBox.Core.Navigation; using XFEEDToolBox.Core.Session; using XFEExtension.NetCore.XUnit.Attributes; namespace XFEEDToolBox.Test; [TestFixture] [NonParallel] // Exercises the process-wide flight input interlock. public class NativeFlightTestProtectionTests { private sealed class Sessions:IGameSessionDriver { public bool CanRelog=>false; public readonly List Calls=[]; public GameSessionTarget Target=new(12,DateTimeOffset.UtcNow.AddMinutes(-2).UtcTicks,"fixture-game","fixture-launcher",false); public Task FindTargetAsync(CancellationToken t){Calls.Add("find");return Task.FromResult(Target);} public Task FindLaunchTargetAsync(CancellationToken t)=>throw new NotSupportedException(); public Task ValidateLaunchAsync(GameSessionTarget p,CancellationToken t){Calls.Add("validate-launch");return Task.CompletedTask;} public Task ValidateRestartAsync(GameSessionTarget p,CancellationToken t)=>throw new NotSupportedException(); public Task ValidateExitAsync(GameSessionTarget p,CancellationToken t){Check.Equal(Target,p);Calls.Add("validate-exit");return Task.CompletedTask;} public Task TerminateAsync(GameSessionTarget p,CancellationToken t){Calls.Add("exit");return Task.CompletedTask;} public Task HasExitedAsync(GameSessionTarget p,CancellationToken t)=>Task.FromResult(true); public Task LaunchAsync(GameSessionTarget p,CancellationToken t){Calls.Add("launch");return Task.CompletedTask;} public Task FindReplacementAsync(GameSessionTarget p,CancellationToken t)=>Task.FromResult(p with{ProcessId=13,StartedUtcTicks=p.StartedUtcTicks+1}); public Task RelogAsync(GameSessionTarget p,CancellationToken t)=>throw new NotSupportedException(); } private static async Task ServeDescription(int id,object response,CancellationToken token) { await using var server=new NamedPipeServerStream($"XFEEDToolBox.Navigation.{id}",PipeDirection.InOut,1,PipeTransmissionMode.Byte,PipeOptions.Asynchronous); await server.WaitForConnectionAsync(token); var buffer=new byte[2048];var count=await server.ReadAsync(buffer,token); using var request=JsonDocument.Parse(buffer.AsMemory(0,count));Check.Equal("describe",request.RootElement.GetProperty("op").GetString()); var bytes=JsonSerializer.SerializeToUtf8Bytes(new{ok=true,result=response}); await server.WriteAsync(bytes,token);await server.WriteAsync(new byte[]{10},token);server.WaitForPipeDrain(); } [Test] public async Task MissingContinuousHealthBlocksLiveTestsBeforeSessionActionsOrFlightInput() { var id=Random.Shared.Next(180000000,190000000);using var timeout=new CancellationTokenSource(TimeSpan.FromSeconds(5)); var sessions=new Sessions();var hash=new string('a',64);var server=ServeDescription(id,new{schema=1,name="XFEEDToolBox.Navigation",verified=true,processId=id,gameSha256=hash},timeout.Token); try{await using var protection=await NativeFlightTestProtection.ArmAsync(id,hash,sessions,()=>new(),()=>{},timeout.Token);throw new Exception("Test was incorrectly armed");} catch(NotSupportedException){} await server;Check.Equal(0,sessions.Calls.Count); } [Test] public async Task ClaimedHealthCannotOverrideWrongProcessOrImageIdentity() { foreach(var wrongProcess in new[]{false,true}) { var id=Random.Shared.Next(190000000,200000000);using var timeout=new CancellationTokenSource(TimeSpan.FromSeconds(5)); var sessions=new Sessions();var hash=new string('a',64); var server=ServeDescription(id,new{schema=1,name="XFEEDToolBox.Navigation",verified=true,processId=wrongProcess?id+1:id,gameSha256=wrongProcess?hash:new string('b',64),continuousShipHealth=true},timeout.Token); try{await using var protection=await NativeFlightTestProtection.ArmAsync(id,hash,sessions,()=>new(),()=>{},timeout.Token);throw new Exception("Test was incorrectly armed");} catch(NotSupportedException){} await server;Check.Equal(0,sessions.Calls.Count); } } [Test] public async Task FailedWorkerAndUiCallbacksCannotPreventDamageEmergencyExit() { var now=DateTimeOffset.UtcNow;var sessions=new Sessions(); var player=new PlayerState{CommanderId="F1",InGame=true,CurrentShipId=7,LoadGameAt=now.AddMinutes(-1),StatusAt=now, Status=new((1L<<24)|8,0,1,1,0,null,null,null,null,[]),Ships=[new(){ShipId=7,HullHealth=1,HullHealthAt=now,ConfigurationAt=now}]}; await using var monitor=new ShipProtectionMonitor(sessions,()=>player,()=>throw new InvalidOperationException("Fixture worker failure")); monitor.Progress+=_=>throw new InvalidOperationException("Fixture UI failure");await monitor.EnableAsync();now=DateTimeOffset.UtcNow; monitor.Observe(new(1,now,sessions.Target.ProcessId,sessions.Target.StartedUtcTicks,99,7,1,null,false),now); monitor.Observe(new(2,now,sessions.Target.ProcessId,sessions.Target.StartedUtcTicks,99,7,.49,null,true),now); await monitor.Completion.WaitAsync(TimeSpan.FromSeconds(3)); Check.That(monitor.TestingBlocked);Check.Equal("find,validate-exit,exit,validate-launch,launch",string.Join(',',sessions.Calls)); monitor.Observe(new(3,now,sessions.Target.ProcessId,sessions.Target.StartedUtcTicks,99,7,.48,null,true),now); Check.Equal(1,sessions.Calls.Count(x=>x=="exit")); } [Test] public async Task DuplicateFreshPhysicsFrameDoesNotDisarmContinuousProtection() { var pid=Random.Shared.Next(200000000,210000000);using var timeout=new CancellationTokenSource(TimeSpan.FromSeconds(5)); var sessions=new Sessions();sessions.Target=sessions.Target with{ProcessId=pid};var hash=new string('a',64);var at=DateTimeOffset.UtcNow; var player=new PlayerState{CommanderId="F1",InGame=true,CurrentShipId=7,LoadGameAt=at.AddMinutes(-1),Ships=[new(){ShipId=7,HullHealth=1,HullHealthAt=at,ConfigurationAt=at}]}; long reads=0; var serve=Task.Run(async()=> { try { while(!timeout.IsCancellationRequested) { await using var server=new NamedPipeServerStream($"XFEEDToolBox.Navigation.{pid}",PipeDirection.InOut,1,PipeTransmissionMode.Byte,PipeOptions.Asynchronous); await server.WaitForConnectionAsync(timeout.Token);var buffer=new byte[4096];var count=await server.ReadAsync(buffer,timeout.Token); using var request=JsonDocument.Parse(buffer.AsMemory(0,count));object result; if(request.RootElement.GetProperty("op").GetString()=="describe")result=new{schema=1,name="XFEEDToolBox.Navigation",verified=true,processId=pid,gameSha256=hash,continuousShipHealth=true}; else { Interlocked.Increment(ref reads); result=new{entityId="99",localPlayerShipVerified=true,shipHealth=new{verified=true,sequence=7,observedAtUnixMilliseconds=at.ToUnixTimeMilliseconds(),entityId="99",hullFraction=1.0,shieldFraction=(double?)null,damageObserved=false}}; } await server.WriteAsync(JsonSerializer.SerializeToUtf8Bytes(new{ok=true,result}),timeout.Token);await server.WriteAsync(new byte[]{10},timeout.Token);server.WaitForPipeDrain(); } } catch(OperationCanceledException)when(timeout.IsCancellationRequested){} }); try { await using var protection=await NativeFlightTestProtection.ArmAsync(pid,hash,sessions,()=>player,()=>{},timeout.Token); await Task.Delay(150,timeout.Token);Check.That(Interlocked.Read(ref reads)>=2);Check.That(!protection.Stopped);Check.That(protection.Fresh); NativeFlightTestProtection.CheckBeforeInput(pid);Check.That(!sessions.Calls.Contains("exit")); } finally{timeout.Cancel();await serve;} } private static async Task ServeHealth(int pid,string hash,Func entity,CancellationToken token,Func? hull=null,bool independent=false,Func? observedAt=null,Func? healthFailure=null,Func? damage=null) { long sequence=0;double? previousHull=null; try { while(!token.IsCancellationRequested) { await using var server=new NamedPipeServerStream($"XFEEDToolBox.Navigation.{pid}",PipeDirection.InOut,1,PipeTransmissionMode.Byte,PipeOptions.Asynchronous); try { await server.WaitForConnectionAsync(token);var bytes=new byte[4096];var count=await server.ReadAsync(bytes,token);if(count==0)continue; using var request=JsonDocument.Parse(bytes.AsMemory(0,count));object result;string? refusal=null; if(request.RootElement.GetProperty("op").GetString()=="describe")result=new{schema=1,name="XFEEDToolBox.Navigation",verified=true,processId=pid,gameSha256=hash,continuousShipHealth=true,independentShipHealth=independent}; else { Check.Equal(independent?"health.read":"read",request.RootElement.GetProperty("op").GetString()); sequence++;var id=entity(sequence).ToString(System.Globalization.CultureInfo.InvariantCulture);refusal=healthFailure?.Invoke(sequence); var health=hull?.Invoke(sequence)??1.0; var damageObserved=damage?.Invoke(sequence)??(previousHull is {} previous&&health99,stop.Token,_=>Volatile.Read(ref damaged)==0?.4:.39,independent:true); try { await using var protection=await NativeFlightTestProtection.ArmAsync(pid,hash,sessions,()=>player,()=>Interlocked.Increment(ref stops),stop.Token,automaticExit:false); Check.That(protection.Fresh&&!protection.Stopped);NativeFlightTestProtection.CheckBeforeInput(pid); Interlocked.Exchange(ref damaged,1); while(!protection.Stopped)await Task.Delay(20,stop.Token); Check.That(!protection.Fresh&&Volatile.Read(ref stops)>0); Check.Throws(()=>NativeFlightTestProtection.CheckBeforeInput(pid)); var pausedSequence=protection.LastHealth!.Sequence; while(!protection.Fresh)await Task.Delay(30,stop.Token); Check.That(!protection.Stopped&&protection.LastHealth!.Sequence>pausedSequence+10); NativeFlightTestProtection.CheckBeforeInput(pid);Check.That(!sessions.Calls.Contains("exit")&&!sessions.Calls.Contains("launch")); Check.Equal(.39,protection.LastHealth.HullHealth); } finally{stop.Cancel();await server;} } [Test] public async Task ManualVisualHandoffStopsInputsWithoutExitOrRestart() { var pid=Random.Shared.Next(280000000,290000000);using var stop=new CancellationTokenSource(TimeSpan.FromSeconds(5)); var sessions=new Sessions();sessions.Target=sessions.Target with{ProcessId=pid};var hash=new string('a',64);var now=DateTimeOffset.UtcNow; var player=new PlayerState{CommanderId="F1",InGame=true,CurrentShipId=7,LoadGameAt=now.AddMinutes(-1),Ships=[new(){ShipId=7,HullHealth=1,HullHealthAt=now,ConfigurationAt=now}]}; var server=ServeHealth(pid,hash,_=>99,stop.Token,independent:true); try { await using var protection=await NativeFlightTestProtection.ArmAsync(pid,hash,sessions,()=>player,()=>{},stop.Token,automaticExit:false); await NativeFlightTestProtection.ExitForFailedHandoffAsync(pid); Check.That(protection.Stopped);Check.Throws(()=>NativeFlightTestProtection.CheckBeforeInput(pid)); Check.That(!sessions.Calls.Contains("exit")&&!sessions.Calls.Contains("launch")&&!sessions.Calls.Contains("validate-exit")); } finally{stop.Cancel();await server;} } [Test] public async Task PreservedHandoffSceneStillExitsForSubsequentRealDamage() { var pid=Random.Shared.Next(300000000,310000000);using var stop=new CancellationTokenSource(TimeSpan.FromSeconds(5)); var sessions=new Sessions();sessions.Target=sessions.Target with{ProcessId=pid};var hash=new string('a',64);var now=DateTimeOffset.UtcNow;var damaged=0; var player=new PlayerState{CommanderId="F1",InGame=true,CurrentShipId=7,LoadGameAt=now.AddMinutes(-1),Ships=[new(){ShipId=7,HullHealth=1,HullHealthAt=now,ConfigurationAt=now}], StatusAt=now,Status=new((1L<<24)|8,0,1,1,0,null,null,null,null,[])}; var server=ServeHealth(pid,hash,_=>99,stop.Token,_=>Volatile.Read(ref damaged)==0?1:.49,independent:true); try { await using var protection=await NativeFlightTestProtection.ArmAsync(pid,hash,sessions,()=>player,()=>{},stop.Token,exitForFailedHandoff:false); while(protection.LastHealth!.Sequence<3)await Task.Delay(20,stop.Token); await NativeFlightTestProtection.ExitForFailedHandoffAsync(pid); Check.That(protection.Stopped);Check.Throws(()=>NativeFlightTestProtection.CheckBeforeInput(pid)); Check.That(!sessions.Calls.Contains("exit")&&!sessions.Calls.Contains("launch")); Interlocked.Exchange(ref damaged,1); while(!sessions.Calls.Contains("exit"))await Task.Delay(20,stop.Token); Check.Equal(1,sessions.Calls.Count(x=>x=="exit")); } finally{stop.Cancel();await server;} } [Test] public async Task ManualVisualDamageStopsInputsWithoutExitOrRestart() { var pid=Random.Shared.Next(290000000,300000000);using var stop=new CancellationTokenSource(TimeSpan.FromSeconds(5)); var sessions=new Sessions();sessions.Target=sessions.Target with{ProcessId=pid};var hash=new string('a',64);var now=DateTimeOffset.UtcNow; var player=new PlayerState{CommanderId="F1",InGame=true,CurrentShipId=7,LoadGameAt=now.AddMinutes(-1),Ships=[new(){ShipId=7,HullHealth=1,HullHealthAt=now,ConfigurationAt=now}]}; player=player with{StatusAt=now,Status=new((1L<<24)|8,0,1,1,0,null,null,null,null,[])}; var damaged=0;var server=ServeHealth(pid,hash,_=>99,stop.Token,_=>Volatile.Read(ref damaged)==0?1:.49,independent:true); try { await using var protection=await NativeFlightTestProtection.ArmAsync(pid,hash,sessions,()=>player,()=>{},stop.Token,automaticExit:false); // The millisecond native timestamp can precede EnableTarget by a // fraction of a millisecond. Establish an accepted healthy baseline // before injecting new damage, rather than racing the initial read. while(protection.LastHealth!.Sequence<3)await Task.Delay(20,stop.Token); Interlocked.Exchange(ref damaged,1); while(!protection.Stopped)await Task.Delay(20,stop.Token); Check.Throws(()=>NativeFlightTestProtection.CheckBeforeInput(pid)); Check.That(!sessions.Calls.Contains("exit")&&!sessions.Calls.Contains("launch")&&!sessions.Calls.Contains("validate-exit")); } finally{stop.Cancel();await server;} } [Test] public async Task IndependentJournalProtectionSurvivesFailedNavigationCleanup() { var pid=Random.Shared.Next(310000000,320000000);using var stop=new CancellationTokenSource(TimeSpan.FromSeconds(5)); var sessions=new Sessions();sessions.Target=sessions.Target with{ProcessId=pid};var hash=new string('a',64);var now=DateTimeOffset.UtcNow; var player=new PlayerState{CommanderId="F1",InGame=true,CurrentShipId=7,LoadGameAt=now.AddMinutes(-1),StatusAt=now, Status=new((1L<<24)|8,0,1,1,0,null,null,null,null,[]),Ships=[new(){ShipId=7,HullHealth=1,HullHealthAt=now,ConfigurationAt=now}]}; await using var independent=new ShipProtectionMonitor(sessions,()=>player,()=>{}); await independent.EnableAsync(stop.Token);var server=ServeHealth(pid,hash,_=>99,stop.Token,independent:true); try { await using(var navigation=await NativeFlightTestProtection.ArmAsync(pid,hash,sessions,()=>player,()=>{},stop.Token,automaticExit:false)) { await NativeFlightTestProtection.ExitForFailedHandoffAsync(pid); Check.That(navigation.Stopped);Check.That(!sessions.Calls.Contains("exit")); } // The failed navigation and its native monitor are both disposed. // The independently owned journal guard must still process damage. var at=DateTimeOffset.UtcNow; independent.Observe(new(1,at,Check.Event(new{timestamp=at,@event="HullDamage",Health=.35,PlayerPilot=true,Fighter=false})),player); while(!sessions.Calls.Contains("exit"))await Task.Delay(20,stop.Token); Check.Equal(1,sessions.Calls.Count(x=>x=="exit")); await independent.Completion; } finally{stop.Cancel();await server;} } [Test] public async Task LostIndependentAuthorityBarsInputEvenWhenLocalHealthIsFresh() { var pid=Random.Shared.Next(320000000,330000000);using var stop=new CancellationTokenSource(TimeSpan.FromSeconds(5)); var sessions=new Sessions();sessions.Target=sessions.Target with{ProcessId=pid};var hash=new string('a',64);var now=DateTimeOffset.UtcNow;var live=true; var player=new PlayerState{CommanderId="F1",InGame=true,CurrentShipId=7,LoadGameAt=now.AddMinutes(-1),Ships=[new(){ShipId=7,HullHealth=1,HullHealthAt=now,ConfigurationAt=now}]}; var server=ServeHealth(pid,hash,_=>99,stop.Token,independent:true); try { await using var p=await NativeFlightTestProtection.ArmAsync(pid,hash,sessions,()=>player,()=>{},stop.Token,automaticExit:false, externalProtectionCheck:()=>{if(!live)throw new InvalidOperationException("Independent protection lost.");}); NativeFlightTestProtection.CheckBeforeInput(pid);live=false;Check.That(p.Fresh); Check.Throws(()=>NativeFlightTestProtection.CheckBeforeInput(pid));Check.That(!sessions.Calls.Contains("exit")); } finally{stop.Cancel();await server;} } [Test] public async Task InitialSceneHealthRefusalWaitsForFreshOriginalHealthBeforeArming() { var pid=Random.Shared.Next(260000000,270000000);using var stop=new CancellationTokenSource(TimeSpan.FromSeconds(5));var sessions=new Sessions();sessions.Target=sessions.Target with{ProcessId=pid}; var hash=new string('a',64);var at=DateTimeOffset.UtcNow;var ready=0;var reads=0; var player=new PlayerState{CommanderId="F1",InGame=true,CurrentShipId=7,LoadGameAt=at.AddMinutes(-1),Ships=[new(){ShipId=7,HullHealth=1,HullHealthAt=at,ConfigurationAt=at}]}; var server=ServeHealth(pid,hash,_=>99,stop.Token,independent:true,healthFailure:_=>{Interlocked.Increment(ref reads);return Volatile.Read(ref ready)==0?"Native main-ship health unavailable or stale":null;}); try { var arm=NativeFlightTestProtection.ArmAsync(pid,hash,sessions,()=>player,()=>{},stop.Token,automaticExit:false); while(Volatile.Read(ref reads)<2)await Task.Delay(10,stop.Token); Check.That(!arm.IsCompleted);Interlocked.Exchange(ref ready,1); await using var protection=await arm; Check.That(protection.Fresh);Check.That(!protection.Stopped);NativeFlightTestProtection.CheckBeforeInput(pid);Check.That(!sessions.Calls.Contains("exit")); } finally{stop.Cancel();await server;} } [Test] public async Task WaitingForInitialHealthStillHonoursCancellation() { var pid=Random.Shared.Next(280000000,290000000);using var stop=new CancellationTokenSource(TimeSpan.FromSeconds(5));var sessions=new Sessions();sessions.Target=sessions.Target with{ProcessId=pid}; var hash=new string('a',64);var at=DateTimeOffset.UtcNow;var reads=0; var player=new PlayerState{CommanderId="F1",InGame=true,CurrentShipId=7,LoadGameAt=at.AddMinutes(-1),Ships=[new(){ShipId=7,HullHealth=1,HullHealthAt=at,ConfigurationAt=at}]}; var server=ServeHealth(pid,hash,_=>99,stop.Token,independent:true,healthFailure:_=>{Interlocked.Increment(ref reads);return "Native main-ship health unavailable or stale";}); try { var arm=NativeFlightTestProtection.ArmAsync(pid,hash,sessions,()=>player,()=>{},stop.Token,automaticExit:false); while(Volatile.Read(ref reads)<2)await Task.Delay(10,stop.Token); stop.Cancel();var cancelled=false;try{await using var protection=await arm;}catch(OperationCanceledException){cancelled=true;} Check.That(cancelled);Check.That(!sessions.Calls.Contains("exit")); } finally{stop.Cancel();await server;} } [Test] public async Task InitialDamageProtectionRefusalIsNeverRetriedAsMissingTelemetry() { var pid=Random.Shared.Next(270000000,280000000);using var stop=new CancellationTokenSource(TimeSpan.FromSeconds(5));var sessions=new Sessions();sessions.Target=sessions.Target with{ProcessId=pid}; var hash=new string('a',64);var at=DateTimeOffset.UtcNow;var reads=0; var player=new PlayerState{CommanderId="F1",InGame=true,CurrentShipId=7,LoadGameAt=at.AddMinutes(-1),Ships=[new(){ShipId=7,HullHealth=1,HullHealthAt=at,ConfigurationAt=at}]}; var server=ServeHealth(pid,hash,_=>99,stop.Token,independent:true,healthFailure:_=>{Interlocked.Increment(ref reads);return "Ship damage protection has stopped flight control";}); try { var refused=false;try{await using var protection=await NativeFlightTestProtection.ArmAsync(pid,hash,sessions,()=>player,()=>{},stop.Token,automaticExit:false);} catch(InvalidOperationException error)when(error.Message=="Ship damage protection has stopped flight control"){refused=true;} Check.That(refused);Check.Equal(1,reads); } finally{stop.Cancel();await server;} } [Test] public async Task BriefHealthStallBarsEveryInputThenResumesOnlyWithFreshOriginalSamples() { var pid=Random.Shared.Next(240000000,250000000);using var stop=new CancellationTokenSource(TimeSpan.FromSeconds(5));var sessions=new Sessions();sessions.Target=sessions.Target with{ProcessId=pid}; var hash=new string('a',64);var at=DateTimeOffset.UtcNow;var resume=0; var player=new PlayerState{CommanderId="F1",InGame=true,CurrentShipId=7,LoadGameAt=at.AddMinutes(-1),Ships=[new(){ShipId=7,HullHealth=1,HullHealthAt=at,ConfigurationAt=at}]}; var server=ServeHealth(pid,hash,_=>99,stop.Token,independent:true,observedAt:()=>Volatile.Read(ref resume)==0?at:DateTimeOffset.UtcNow); try { await using var protection=await NativeFlightTestProtection.ArmAsync(pid,hash,sessions,()=>player,()=>{},stop.Token); await Task.Delay(650,stop.Token); Check.That(!protection.Stopped);Check.That(!protection.Fresh); Check.Throws(()=>NativeFlightTestProtection.CheckBeforeInput(pid)); foreach(var operation in new[]{"commerce.trade","commerce.trade.batch","cg.join","station.services"}) { var refused=false; try{await new NavigationPipeClient(pid).CallAsync(new{op=operation},stop.Token);} catch(FlightTelemetryUnavailableException){refused=true;} Check.That(refused); // no mutating request may reach the fixture's pipe } Interlocked.Exchange(ref resume,1); while(!protection.Fresh)await Task.Delay(20,stop.Token); NativeFlightTestProtection.CheckBeforeInput(pid);Check.That(!sessions.Calls.Contains("exit")); } finally{stop.Cancel();await server;} } [Test] public async Task IndependentHealthDoesNotDependOnReadableFlightGeometry() { var pid=Random.Shared.Next(230000000,240000000);using var stop=new CancellationTokenSource(TimeSpan.FromSeconds(5));var sessions=new Sessions();sessions.Target=sessions.Target with{ProcessId=pid}; var hash=new string('a',64);var now=DateTimeOffset.UtcNow;var player=new PlayerState{CommanderId="F1",InGame=true,CurrentShipId=7,LoadGameAt=now.AddMinutes(-1),Ships=[new(){ShipId=7,HullHealth=1,HullHealthAt=now,ConfigurationAt=now}]}; var server=ServeHealth(pid,hash,_=>99,stop.Token,independent:true); try { await using var protection=await NativeFlightTestProtection.ArmAsync(pid,hash,sessions,()=>player,()=>{},stop.Token); await Task.Delay(120,stop.Token);Check.That(protection.Fresh);Check.That(!protection.Stopped);Check.Equal(1d,protection.LastHealth!.HullHealth); } finally{stop.Cancel();await server;} } [Test]public async Task LoadingWitnessStillBarsExpiredInputAndRecoveryRequiresAnActualFreshHealthRead() { var pid=Random.Shared.Next(380000000,390000000);using var stop=new CancellationTokenSource(TimeSpan.FromSeconds(6)); var sessions=new Sessions();sessions.Target=sessions.Target with{ProcessId=pid};var hash=new string('a',64);var at=DateTimeOffset.UtcNow;var resume=0;var pauses=0; var phases=new System.Collections.Concurrent.ConcurrentQueue(); var player=new PlayerState{CommanderId="F1",InGame=true,GameMode="Solo",CurrentShipId=7,LoadGameAt=at.AddMinutes(-1),Ships=[new(){ShipId=7,HullHealth=1,HullHealthAt=at,ConfigurationAt=at}]}; var server=ServeHealth(pid,hash,_=>99,stop.Token,independent:true,observedAt:()=>Volatile.Read(ref resume)==0?at:DateTimeOffset.UtcNow); try { await using var protection=await NativeFlightTestProtection.ArmAsync(pid,hash,sessions,()=>player,()=>{},stop.Token,automaticExit:false, expectedInputFreeJumpLoading:()=>true,pauseAutomation:_=>Interlocked.Increment(ref pauses),diagnostics:diagnostic=>phases.Enqueue(diagnostic.Phase)); await Task.Delay(650,stop.Token);Check.That(!protection.Fresh&&!protection.Stopped);Check.Equal(0,pauses); Check.Throws(()=>NativeFlightTestProtection.CheckBeforeInput(pid)); Interlocked.Exchange(ref resume,1);while(!protection.Fresh)await Task.Delay(20,stop.Token); NativeFlightTestProtection.CheckBeforeInput(pid); Check.Equal(1,phases.Count(phase=>phase=="health-gap"));Check.Equal(1,phases.Count(phase=>phase=="health-recovered")); Check.Equal(0,pauses);Check.That(!sessions.Calls.Contains("exit")); } finally{stop.Cancel();await server;} } [Test]public async Task UnknownHealthFailurePersistsItsActualCauseBeforePausingInsteadOfCallingManualStop() { var pid=Random.Shared.Next(390000000,400000000);using var stop=new CancellationTokenSource(TimeSpan.FromSeconds(5)); var sessions=new Sessions();sessions.Target=sessions.Target with{ProcessId=pid};var hash=new string('a',64);var at=DateTimeOffset.UtcNow;var manualStops=0; var evidence=new System.Collections.Concurrent.ConcurrentQueue(); var paused=new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var player=new PlayerState{CommanderId="F1",InGame=true,GameMode="Solo",CurrentShipId=7,LoadGameAt=at.AddMinutes(-1),Ships=[new(){ShipId=7,HullHealth=1,HullHealthAt=at,ConfigurationAt=at}]}; var server=ServeHealth(pid,hash,_=>99,stop.Token,independent:true,healthFailure:sequence=>sequence>1?"Unknown original health source":null); try { await using var protection=await NativeFlightTestProtection.ArmAsync(pid,hash,sessions,()=>player,()=>Interlocked.Increment(ref manualStops),stop.Token,automaticExit:false, expectedInputFreeJumpLoading:()=>true,pauseAutomation:error=> {Check.That(evidence.Any(item=>item.Phase=="monitoring-paused"));paused.TrySetResult(error);},diagnostics:evidence.Enqueue); var reason=await paused.Task.WaitAsync(stop.Token);Check.That(reason is InvalidOperationException);Check.Equal("Unknown original health source",reason.Message); await Task.Delay(160,stop.Token); var diagnostic=evidence.Single(item=>item.Phase=="monitoring-paused"); Check.Equal(pid,diagnostic.ProcessId);Check.Equal(sessions.Target.StartedUtcTicks,diagnostic.StartedUtcTicks);Check.Equal("Solo",diagnostic.GameMode); Check.Equal(1L,diagnostic.LastHealthSequence!.Value);Check.Equal(99UL,diagnostic.LastHealthEntity!.Value); Check.Equal(reason.Message,diagnostic.ExceptionMessage!);Check.That(diagnostic.UnavailableSince is not null); Check.That(protection.Stopped&&!protection.Fresh);Check.Equal(0,manualStops);Check.That(!sessions.Calls.Contains("exit")); } finally{stop.Cancel();await server;} } [Test]public async Task ActualDamageUsesTheExistingSafetyStopEvenWithAReleasedJumpLoadingWitness() { var pid=Random.Shared.Next(400000000,410000000);using var stop=new CancellationTokenSource(TimeSpan.FromSeconds(5)); var sessions=new Sessions();sessions.Target=sessions.Target with{ProcessId=pid};var hash=new string('a',64);var at=DateTimeOffset.UtcNow;var safetyStops=0;var monitoringPauses=0; var phases=new System.Collections.Concurrent.ConcurrentQueue(); var player=new PlayerState{CommanderId="F1",InGame=true,GameMode="Solo",CurrentShipId=7,LoadGameAt=at.AddMinutes(-1),StatusAt=at, Status=new((1L<<24)|8,0,1,1,0,null,null,null,null,[]),Ships=[new(){ShipId=7,HullHealth=1,HullHealthAt=at,ConfigurationAt=at}]}; var server=ServeHealth(pid,hash,_=>99,stop.Token,hull:sequence=>sequence<4?1:.49,independent:true); try { await using var protection=await NativeFlightTestProtection.ArmAsync(pid,hash,sessions,()=>player,()=> {Check.That(phases.Contains("safety-stop"));Interlocked.Increment(ref safetyStops);},stop.Token,automaticExit:false, expectedInputFreeJumpLoading:()=>true,pauseAutomation:_=>Interlocked.Increment(ref monitoringPauses),diagnostics:diagnostic=>phases.Enqueue(diagnostic.Phase)); while(Volatile.Read(ref safetyStops)==0)await Task.Delay(20,stop.Token); Check.That(protection.Stopped&&!protection.Fresh);Check.Equal(0,monitoringPauses);Check.That(!sessions.Calls.Contains("exit")); } finally{stop.Cancel();await server;} } [Test] public async Task ProductionMonitorSharesNativeFeedAndDisposalReleasesTheInputInterlock() { var pid=Random.Shared.Next(210000000,220000000);using var stop=new CancellationTokenSource(TimeSpan.FromSeconds(5));var sessions=new Sessions();sessions.Target=sessions.Target with{ProcessId=pid}; var hash=new string('a',64);var now=DateTimeOffset.UtcNow;var player=new PlayerState{CommanderId="F1",InGame=true,CurrentShipId=7,LoadGameAt=now.AddMinutes(-1),Ships=[new(){ShipId=7,HullHealth=1,HullHealthAt=now,ConfigurationAt=now}]}; var server=ServeHealth(pid,hash,_=>99,stop.Token); try { var monitor=new ShipProtectionMonitor(sessions,()=>player,()=>{});await monitor.EnableAsync(stop.Token); await monitor.AttachNativeAsync(hash,stop.Token);Check.That(monitor.ContinuousHealthReady);monitor.RequireHealthyShip(requireContinuous:true); await monitor.DisposeAsync().AsTask().WaitAsync(TimeSpan.FromSeconds(1)); await using var independent=await NativeFlightTestProtection.ArmAsync(pid,hash,sessions,()=>player,()=>{},stop.Token); Check.That(independent.Fresh);NativeFlightTestProtection.CheckBeforeInput(pid);Check.That(!sessions.Calls.Contains("exit")); } finally{stop.Cancel();await server;} } [Test] public async Task ChangedNativeShipImmediatelyBarsInputWithoutAnEmergencyForHealthyNewShip() { var pid=Random.Shared.Next(220000000,230000000);using var stop=new CancellationTokenSource(TimeSpan.FromSeconds(5));var sessions=new Sessions();sessions.Target=sessions.Target with{ProcessId=pid}; var hash=new string('a',64);var now=DateTimeOffset.UtcNow;var player=new PlayerState{CommanderId="F1",InGame=true,CurrentShipId=7,LoadGameAt=now.AddMinutes(-1),Ships=[new(){ShipId=7,HullHealth=1,HullHealthAt=now,ConfigurationAt=now}]}; var server=ServeHealth(pid,hash,sequence=>sequence==1?99UL:100UL,stop.Token);var stopped=0; try { await using var protection=await NativeFlightTestProtection.ArmAsync(pid,hash,sessions,()=>player,()=>{Interlocked.Increment(ref stopped);throw new InvalidOperationException("Fixture stopped worker");},stop.Token); await Task.Delay(120,stop.Token);Check.That(protection.Stopped);Check.Equal(0,stopped); Check.Equal(100UL,protection.LastHealth!.EntityId); Check.Throws(()=>NativeFlightTestProtection.CheckBeforeInput(pid));Check.That(!sessions.Calls.Contains("exit")); } finally{stop.Cancel();await server;} } [Test] public async Task ChangedNativeShipKeepsDamageProtectionAfterOldInputsAreBarred() { var pid=Random.Shared.Next(230000000,240000000);using var stop=new CancellationTokenSource(TimeSpan.FromSeconds(5));var sessions=new Sessions();sessions.Target=sessions.Target with{ProcessId=pid}; var hash=new string('a',64);var now=DateTimeOffset.UtcNow;var player=new PlayerState{CommanderId="F1",InGame=true,CurrentShipId=7,LoadGameAt=now.AddMinutes(-1),Ships=[new(){ShipId=7,HullHealth=1,HullHealthAt=now,ConfigurationAt=now}]}; player=player with{StatusAt=now,Status=new((1L<<24)|8,0,1,1,0,null,null,null,null,[])}; var server=ServeHealth(pid,hash,sequence=>sequence==1?99UL:100UL,stop.Token,sequence=>sequence<5?1:.49); try { await using var protection=await NativeFlightTestProtection.ArmAsync(pid,hash,sessions,()=>player,()=>throw new InvalidOperationException("Fixture stopped worker"),stop.Token); while(!sessions.Calls.Contains("launch"))await Task.Delay(20,stop.Token); Check.That(protection.Stopped);Check.That(protection.LastHealth!.Sequence>=5);Check.Equal(100UL,protection.LastHealth.EntityId); Check.Throws(()=>NativeFlightTestProtection.CheckBeforeInput(pid));Check.Equal(1,sessions.Calls.Count(x=>x=="exit")); } finally{stop.Cancel();await server;} } }