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<string> Calls=[];
public GameSessionTarget Target=new(12,DateTimeOffset.UtcNow.AddMinutes(-2).UtcTicks,"fixture-game","fixture-launcher",false);
public Task<GameSessionTarget> FindTargetAsync(CancellationToken t){Calls.Add("find");return Task.FromResult(Target);}
public Task<GameSessionTarget> 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<bool> HasExitedAsync(GameSessionTarget p,CancellationToken t)=>Task.FromResult(true);
public Task LaunchAsync(GameSessionTarget p,CancellationToken t){Calls.Add("launch");return Task.CompletedTask;}
public Task<GameSessionTarget?> FindReplacementAsync(GameSessionTarget p,CancellationToken t)=>Task.FromResult<GameSessionTarget?>(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<long,ulong> entity,CancellationToken token,Func<long,double>? hull=null,bool independent=false,Func<DateTimeOffset>? observedAt=null,Func<long,string?>? healthFailure=null,Func<long,bool>? 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&&health<previous-.000001);previousHull=health;
result=new{entityId=id,localPlayerShipVerified=true,shipHealth=new{verified=true,sequence,observedAtUnixMilliseconds=(observedAt?.Invoke()??DateTimeOffset.UtcNow).ToUnixTimeMilliseconds(),entityId=id,hullFraction=health,shieldFraction=(double?)null,damageObserved}};
}
object response=refusal is null?new{ok=true,result}:new{ok=false,error=refusal};
await server.WriteAsync(JsonSerializer.SerializeToUtf8Bytes(response),token);await server.WriteAsync(new byte[]{10},token);server.WaitForPipeDrain();
}
catch(IOException)when(!token.IsCancellationRequested){/* A disposed polling client can close before this fixture writes its response. */}
}
}
catch(OperationCanceledException)when(token.IsCancellationRequested){}
catch(IOException)when(token.IsCancellationRequested){}
}
[Test]public async Task HistoricalLowHullCanArmAndNewDamageKeepsPollingUntilUserRetryIsSafe()
{
var pid=Random.Shared.Next(310000000,320000000);using var stop=new CancellationTokenSource(TimeSpan.FromSeconds(8));
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,0,1,1,0,null,null,null,null,[]),Ships=[new(){ShipId=7,HullHealth=.4,HullHealthAt=now,ConfigurationAt=now}]};
var damaged=0;var stops=0;
var server=ServeHealth(pid,hash,_=>99,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<InvalidOperationException>(()=>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<InvalidOperationException>(()=>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<InvalidOperationException>(()=>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<InvalidOperationException>(()=>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<InvalidOperationException>(()=>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<FlightTelemetryUnavailableException>(()=>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<string>();
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<FlightTelemetryUnavailableException>(()=>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<NativeFlightProtectionDiagnostic>();
var paused=new TaskCompletionSource<Exception>(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<string>();
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<InvalidOperationException>(()=>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<InvalidOperationException>(()=>NativeFlightTestProtection.CheckBeforeInput(pid));Check.Equal(1,sessions.Calls.Count(x=>x=="exit"));
}
finally{stop.Cancel();await server;}
}
}
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<string> Calls=[];
public GameSessionTarget Target=new(12,DateTimeOffset.UtcNow.AddMinutes(-2).UtcTicks,"fixture-game","fixture-launcher",false);
public Task<GameSessionTarget> FindTargetAsync(CancellationToken t){Calls.Add("find");return Task.FromResult(Target);}
public Task<GameSessionTarget> 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<bool> HasExitedAsync(GameSessionTarget p,CancellationToken t)=>Task.FromResult(true);
public Task LaunchAsync(GameSessionTarget p,CancellationToken t){Calls.Add("launch");return Task.CompletedTask;}
public Task<GameSessionTarget?> FindReplacementAsync(GameSessionTarget p,CancellationToken t)=>Task.FromResult<GameSessionTarget?>(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<long,ulong> entity,CancellationToken token,Func<long,double>? hull=null,bool independent=false,Func<DateTimeOffset>? observedAt=null,Func<long,string?>? healthFailure=null,Func<long,bool>? 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&&health<previous-.000001);previousHull=health;
result=new{entityId=id,localPlayerShipVerified=true,shipHealth=new{verified=true,sequence,observedAtUnixMilliseconds=(observedAt?.Invoke()??DateTimeOffset.UtcNow).ToUnixTimeMilliseconds(),entityId=id,hullFraction=health,shieldFraction=(double?)null,damageObserved}};
}
object response=refusal is null?new{ok=true,result}:new{ok=false,error=refusal};
await server.WriteAsync(JsonSerializer.SerializeToUtf8Bytes(response),token);await server.WriteAsync(new byte[]{10},token);server.WaitForPipeDrain();
}
catch(IOException)when(!token.IsCancellationRequested){/* A disposed polling client can close before this fixture writes its response. */}
}
}
catch(OperationCanceledException)when(token.IsCancellationRequested){}
catch(IOException)when(token.IsCancellationRequested){}
}
[Test]public async Task HistoricalLowHullCanArmAndNewDamageKeepsPollingUntilUserRetryIsSafe()
{
var pid=Random.Shared.Next(310000000,320000000);using var stop=new CancellationTokenSource(TimeSpan.FromSeconds(8));
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,0,1,1,0,null,null,null,null,[]),Ships=[new(){ShipId=7,HullHealth=.4,HullHealthAt=now,ConfigurationAt=now}]};
var damaged=0;var stops=0;
var server=ServeHealth(pid,hash,_=>99,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<InvalidOperationException>(()=>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<InvalidOperationException>(()=>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<InvalidOperationException>(()=>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<InvalidOperationException>(()=>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<InvalidOperationException>(()=>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<FlightTelemetryUnavailableException>(()=>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<string>();
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<FlightTelemetryUnavailableException>(()=>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<NativeFlightProtectionDiagnostic>();
var paused=new TaskCompletionSource<Exception>(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<string>();
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<InvalidOperationException>(()=>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<InvalidOperationException>(()=>NativeFlightTestProtection.CheckBeforeInput(pid));Check.Equal(1,sessions.Calls.Count(x=>x=="exit"));
}
finally{stop.Cancel();await server;}
}
}