using System.Buffers.Binary;
using XFEEDToolBox.Core.Session;
using XFEExtension.NetCore.XUnit.Attributes;
namespace XFEEDToolBox.Test;
[TestFixture]public class StartupCrashRecoveryTests
{
private static readonly DateTimeOffset At=DateTimeOffset.Parse("2026-10-25T22:33:52Z");
private static GameSessionTarget Target=>new(74248,At.AddSeconds(-15).UtcTicks,"fixture-game","fixture-launcher",false);
private static byte[] Dump(int pid=74248,uint exception=0xc0000005)
{
var data=new byte[256];void Put(int offset,uint value)=>BinaryPrimitives.WriteUInt32LittleEndian(data.AsSpan(offset),value);
Put(0,0x504d444d);Put(8,2);Put(12,32);Put(20,(uint)At.ToUnixTimeSeconds());
Put(32,6);Put(36,12);Put(40,64);Put(72,exception);
Put(44,15);Put(48,12);Put(52,80);Put(80,12);Put(84,1);Put(88,(uint)pid);return data;
}
[Test]public void OnlyFreshExceptionDumpForTheExactNewProcessAuthorizesRecovery()
{
using var temp=new TemporaryDirectory();var path=Path.Combine(temp.Path,"crash.dmp");File.WriteAllBytes(path,Dump());
var proof=StartupCrashRecovery.Read(path,Target,At.AddSeconds(5));Check.That(proof is{ProcessId:74248,ExceptionCode:0xc0000005});
Check.That(StartupCrashRecovery.Read(path,Target with{ProcessId=42},At) is null);
Check.That(StartupCrashRecovery.Read(path,Target with{StartedUtcTicks=At.AddMinutes(1).UtcTicks},At) is null);
Check.That(StartupCrashRecovery.Read(path,Target,At.AddMinutes(6)) is null);
Check.That(StartupCrashRecovery.Read(path,Target,At.AddMinutes(-1)) is null);
File.WriteAllBytes(path,Dump(exception:0));Check.That(StartupCrashRecovery.Read(path,Target,At) is null);
File.WriteAllBytes(path,new byte[8]);Check.That(StartupCrashRecovery.Read(path,Target,At) is null);
}
[Test]public async Task OwnedStartupCrashRetriesOnceAndPreservesOriginalCallerScope()
{
var attempts=0;var recoveries=0;
await StartupCrashRecovery.RunAsync((own,_)=>{own(Target);if(++attempts==1)throw new IOException("Startup pipe ended");return Task.CompletedTask;},
(_,_)=>Task.FromResult(true),t=>new("fixture",t.ProcessId,0xc0000005,At),(_,n,_)=>{recoveries=n;return Task.CompletedTask;},default);
Check.Equal(2,attempts);Check.Equal(1,recoveries);
}
[Test]public async Task OrdinaryExitLiveProcessAndUnownedInstanceAreNeverRelaunched()
{
foreach(var reason in new[]{0,1,2})
{
var attempts=0;var refused=false;
try{await StartupCrashRecovery.RunAsync((own,_)=>{attempts++;if(reason!=2)own(Target);throw new IOException("fixture stop");},
(_,_)=>Task.FromResult(reason!=1),t=>reason==0?null:new("fixture",t.ProcessId,0xc0000005,At),null,default);}
catch(IOException){refused=true;}
Check.That(refused);Check.Equal(1,attempts);
}
}
[Test]public async Task RepeatedOwnedCrashesRemainBoundedAndCancellationCannotRestart()
{
var attempts=0;var refused=false;
try{await StartupCrashRecovery.RunAsync((own,_)=>{attempts++;own(Target);throw new IOException();},(_,_)=>Task.FromResult(true),t=>new("fixture",t.ProcessId,0xc0000005,At),null,default);}
catch(IOException){refused=true;}
Check.That(refused);Check.Equal(3,attempts);
using var stop=new CancellationTokenSource();attempts=0;
try{await StartupCrashRecovery.RunAsync((own,_)=>{attempts++;own(Target);stop.Cancel();throw new OperationCanceledException(stop.Token);},(_,_)=>Task.FromResult(true),t=>new("fixture",t.ProcessId,0xc0000005,At),null,stop.Token);}
catch(OperationCanceledException){}
Check.Equal(1,attempts);
}
[Test]public async Task IdentityCargoAndSafetyRejectionRemainTerminalEvenWithAnOwnedCrashDump()
{
foreach(var failure in new Exception[]{new InvalidDataException("commander or actual cargo changed"),new NotSupportedException("unverified build"),
new XFEEDToolBox.Core.Navigation.FlightThreatPauseException(),new IOException("transport wrapper",new InvalidDataException("original identity mismatch")),
new AggregateException(new IOException("pipe ended"),new InvalidDataException("ship changed"))})
{
var attempts=0;var recoveries=0;var rejected=false;
try{await StartupCrashRecovery.RunAsync((own,_)=>{own(Target);attempts++;throw failure;},(_,_)=>Task.FromResult(true),
target=>new("fixture",target.ProcessId,0xc0000005,At),(_,_,_)=>{recoveries++;return Task.CompletedTask;},default);}
catch(Exception actual){rejected=ReferenceEquals(actual,failure);}
Check.That(rejected);Check.Equal(1,attempts);Check.Equal(0,recoveries);
}
}
}
using System.Buffers.Binary;
using XFEEDToolBox.Core.Session;
using XFEExtension.NetCore.XUnit.Attributes;
namespace XFEEDToolBox.Test;
[TestFixture]public class StartupCrashRecoveryTests
{
private static readonly DateTimeOffset At=DateTimeOffset.Parse("2026-10-25T22:33:52Z");
private static GameSessionTarget Target=>new(74248,At.AddSeconds(-15).UtcTicks,"fixture-game","fixture-launcher",false);
private static byte[] Dump(int pid=74248,uint exception=0xc0000005)
{
var data=new byte[256];void Put(int offset,uint value)=>BinaryPrimitives.WriteUInt32LittleEndian(data.AsSpan(offset),value);
Put(0,0x504d444d);Put(8,2);Put(12,32);Put(20,(uint)At.ToUnixTimeSeconds());
Put(32,6);Put(36,12);Put(40,64);Put(72,exception);
Put(44,15);Put(48,12);Put(52,80);Put(80,12);Put(84,1);Put(88,(uint)pid);return data;
}
[Test]public void OnlyFreshExceptionDumpForTheExactNewProcessAuthorizesRecovery()
{
using var temp=new TemporaryDirectory();var path=Path.Combine(temp.Path,"crash.dmp");File.WriteAllBytes(path,Dump());
var proof=StartupCrashRecovery.Read(path,Target,At.AddSeconds(5));Check.That(proof is{ProcessId:74248,ExceptionCode:0xc0000005});
Check.That(StartupCrashRecovery.Read(path,Target with{ProcessId=42},At) is null);
Check.That(StartupCrashRecovery.Read(path,Target with{StartedUtcTicks=At.AddMinutes(1).UtcTicks},At) is null);
Check.That(StartupCrashRecovery.Read(path,Target,At.AddMinutes(6)) is null);
Check.That(StartupCrashRecovery.Read(path,Target,At.AddMinutes(-1)) is null);
File.WriteAllBytes(path,Dump(exception:0));Check.That(StartupCrashRecovery.Read(path,Target,At) is null);
File.WriteAllBytes(path,new byte[8]);Check.That(StartupCrashRecovery.Read(path,Target,At) is null);
}
[Test]public async Task OwnedStartupCrashRetriesOnceAndPreservesOriginalCallerScope()
{
var attempts=0;var recoveries=0;
await StartupCrashRecovery.RunAsync((own,_)=>{own(Target);if(++attempts==1)throw new IOException("Startup pipe ended");return Task.CompletedTask;},
(_,_)=>Task.FromResult(true),t=>new("fixture",t.ProcessId,0xc0000005,At),(_,n,_)=>{recoveries=n;return Task.CompletedTask;},default);
Check.Equal(2,attempts);Check.Equal(1,recoveries);
}
[Test]public async Task OrdinaryExitLiveProcessAndUnownedInstanceAreNeverRelaunched()
{
foreach(var reason in new[]{0,1,2})
{
var attempts=0;var refused=false;
try{await StartupCrashRecovery.RunAsync((own,_)=>{attempts++;if(reason!=2)own(Target);throw new IOException("fixture stop");},
(_,_)=>Task.FromResult(reason!=1),t=>reason==0?null:new("fixture",t.ProcessId,0xc0000005,At),null,default);}
catch(IOException){refused=true;}
Check.That(refused);Check.Equal(1,attempts);
}
}
[Test]public async Task RepeatedOwnedCrashesRemainBoundedAndCancellationCannotRestart()
{
var attempts=0;var refused=false;
try{await StartupCrashRecovery.RunAsync((own,_)=>{attempts++;own(Target);throw new IOException();},(_,_)=>Task.FromResult(true),t=>new("fixture",t.ProcessId,0xc0000005,At),null,default);}
catch(IOException){refused=true;}
Check.That(refused);Check.Equal(3,attempts);
using var stop=new CancellationTokenSource();attempts=0;
try{await StartupCrashRecovery.RunAsync((own,_)=>{attempts++;own(Target);stop.Cancel();throw new OperationCanceledException(stop.Token);},(_,_)=>Task.FromResult(true),t=>new("fixture",t.ProcessId,0xc0000005,At),null,stop.Token);}
catch(OperationCanceledException){}
Check.Equal(1,attempts);
}
[Test]public async Task IdentityCargoAndSafetyRejectionRemainTerminalEvenWithAnOwnedCrashDump()
{
foreach(var failure in new Exception[]{new InvalidDataException("commander or actual cargo changed"),new NotSupportedException("unverified build"),
new XFEEDToolBox.Core.Navigation.FlightThreatPauseException(),new IOException("transport wrapper",new InvalidDataException("original identity mismatch")),
new AggregateException(new IOException("pipe ended"),new InvalidDataException("ship changed"))})
{
var attempts=0;var recoveries=0;var rejected=false;
try{await StartupCrashRecovery.RunAsync((own,_)=>{own(Target);attempts++;throw failure;},(_,_)=>Task.FromResult(true),
target=>new("fixture",target.ProcessId,0xc0000005,At),(_,_,_)=>{recoveries++;return Task.CompletedTask;},default);}
catch(Exception actual){rejected=ReferenceEquals(actual,failure);}
Check.That(rejected);Check.Equal(1,attempts);Check.Equal(0,recoveries);
}
}
}