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); } } }