using XFEEDToolBox.Core.Models; using XFEEDToolBox.Core.Navigation; using XFEExtension.NetCore.XUnit.Attributes; namespace XFEEDToolBox.Test; [TestFixture] public class SystemNavigationRunnerTests { private static readonly DateTimeOffset Now=DateTimeOffset.Parse("2026-10-22T01:00:00Z"); private static readonly SystemNavigationTarget Target=new(1,10,"Exact station",SystemTargetKind.Station,20); private static PlayerState Player=>new(){CommanderId="F1",CurrentShipId=7,InGame=true,LoadGameAt=Now.AddMinutes(-1),Navigation=new(){CurrentSystemAddress=1},Ships=[new(){ShipId=7,HullHealth=1}]}; private static SystemFlightFrame Docked=>new(){Sequence=1,ObservedAt=Now,GameSha256=new('a',64),LoadoutFingerprint="exact-ship",ShipEntity=17,SystemAddress=1,ReferenceWorld=10, SelectedTarget=Target,GeometryVerified=true,InGame=true,Mode=LocalFlightMode.Docked,DockedMarketId=20, Forward=new(0,0,1),Up=new(0,1,0),MaximumNormalSpeed=100,MaximumCruiseSpeed=1e9,BrakingAcceleration=10,TranslationAcceleration=10,LegalApproachSpeed=100}; private sealed class Fake:ISystemFlightControlDriver { public readonly List Calls=[]; public Func Frame=_=>Docked; public Action? OnPrepare; public Action? OnApply; public Action? OnDecision; public bool Ready=true; private int reads; public Task InspectAsync(CancellationToken t){Calls.Add("inspect");return Task.FromResult(new SystemNavigationCapabilities(new('a',64),Ready,true,true,true,true,true,"Isolated fixture"));} public Task SelectAsync(SystemNavigationTarget target,CancellationToken t){Calls.Add("select");Check.That(target.SameIdentity(Target));return Task.CompletedTask;} public Task PrepareAsync(CancellationToken t){Calls.Add("prepare");OnPrepare?.Invoke();return Task.CompletedTask;} public Task ReadSystemFlightAsync(CancellationToken t){Calls.Add("read");return Task.FromResult(Frame(++reads));} public Task ApplySystemFlightAsync(SystemFlightDecision d,SystemFlightFrame f,SystemNavigationTarget target,DateTimeOffset expires,CancellationToken t) {Calls.Add("apply");Check.That(target.SameIdentity(Target)&&expires==f.ObservedAt.AddMilliseconds(250));OnDecision?.Invoke(d);OnApply?.Invoke();return Task.CompletedTask;} public Task ReleaseSystemFlightAsync(){Calls.Add("release");return Task.CompletedTask;} } [Test] public async Task ExactDockedAcknowledgementCompletesAndAlwaysReleasesControl() { var driver=new Fake();var runner=new SystemNavigationRunner(()=>Player,()=>Now);await runner.RunAsync(Target,driver); Check.Equal(NavigationRunState.Completed,runner.State);Check.Equal("inspect,select,prepare,read,apply,release",string.Join(',',driver.Calls)); } [Test]public async Task DockingAtTheSameMarketDuringPreparationCompletesWithoutReadingOrApplyingFlightInputs() { var player=Player;var driver=new Fake{OnPrepare=()=>player=player with{Docked=true,MarketId=20},Frame=_=>throw new InvalidOperationException("Docked physics need not keep updating.")}; var runner=new SystemNavigationRunner(()=>player,()=>Now);await runner.RunAsync(Target,driver); Check.Equal(NavigationRunState.Completed,runner.State);Check.Equal("inspect,select,prepare,release",string.Join(',',driver.Calls)); } [Test]public async Task ADockedArrivalNeverOverridesAChangedShipOrAnExternalCancellation() { foreach(var cancel in new[]{false,true}) { using var stop=new CancellationTokenSource();var player=Player;var driver=new Fake{OnPrepare=()=> {player=player with{Docked=true,MarketId=20};if(cancel)stop.Cancel();else player=player with{CurrentShipId=8};}}; var runner=new SystemNavigationRunner(()=>player,()=>Now);await runner.RunAsync(Target,driver,stop.Token); Check.Equal(cancel?NavigationRunState.Stopped:NavigationRunState.Failed,runner.State);Check.Equal(0,driver.Calls.Count(call=>call=="read"||call=="apply")); } } [Test]public async Task APreCancelledRequestAtTheDestinationDoesNotReportSuccessfulArrival() { using var stop=new CancellationTokenSource();stop.Cancel();var driver=new Fake(); var runner=new SystemNavigationRunner(()=>Player with{Docked=true,MarketId=20},()=>Now);await runner.RunAsync(Target,driver,stop.Token); Check.Equal(NavigationRunState.Stopped,runner.State);Check.Equal("release",string.Join(',',driver.Calls)); } [Test]public async Task RepeatedStillFreshPhysicsFrameWaitsWithoutReleasingControlOrReplayingInputs() { var at=Now;var driver=new Fake{Frame=n=> { at=Now.AddMilliseconds((n-1)*100); return n<3?Landing(1,Now,NativeAssistState.Ready):Docked with{Sequence=2,ObservedAt=at}; }}; var runner=new SystemNavigationRunner(()=>Player,()=>at);await runner.RunAsync(Target,driver); Check.Equal(NavigationRunState.Completed,runner.State);Check.Equal(3,driver.Calls.Count(call=>call=="read")); Check.Equal(1,driver.Calls.Count(call=>call=="release"));Check.Equal(2,driver.Calls.Count(call=>call=="apply")); } private static SystemFlightFrame Landing(int sequence,DateTimeOffset at,NativeAssistState assist)=>Docked with{Sequence=sequence,ObservedAt=at,Mode=LocalFlightMode.Normal, TargetDistance=7000,TargetPosition=new(0,0,7000),Speed=50,Assists=new(false,true,false),DockingAssist=assist, Clearance=new(20,DockingPermission.Granted,9,Now,Now.AddMinutes(5))}; [Test] public async Task RejectedUndispatchedResetObservesActualAiOnDamagedButProtectedShip() { var at=Now;var arms=0;var ship=Player with{Ships=[new(){ShipId=7,HullHealth=.784498}]}; var driver=new Fake{Frame=n=>{at=Now.AddMilliseconds((n-1)*100);return n==3?Docked with{Sequence=n,ObservedAt=at}:Landing(n,at,n==1?NativeAssistState.Ready:NativeAssistState.Engaged);}, OnDecision=d=>{if(d.Actions.HasFlag(SystemNavigationAction.EnableDockingAssist)){arms++;throw new DockingResetObservationPendingException("rejected-before-dispatch",20,Now,new InvalidOperationException("initial telemetry rejected"));}}}; var runner=new SystemNavigationRunner(()=>ship,()=>at);await runner.RunAsync(Target,driver); Check.Equal(NavigationRunState.Completed,runner.State);Check.Equal(1,arms);Check.Equal(1,driver.Calls.Count(x=>x=="apply"));Check.Equal(1,driver.Calls.Count(x=>x=="release")); } [Test] public async Task LostResetAcknowledgementObservesActualAiAndDockingWithoutRearmingOrSuspending() { var at=Now;var arms=0;var driver=new Fake{Frame=n=>{at=Now.AddMilliseconds((n-1)*100);if(n==2)throw new IOException("Brief telemetry interruption during handoff");return n==4?Docked with{Sequence=n,ObservedAt=at}:Landing(n,at,n==1?NativeAssistState.Ready:NativeAssistState.Engaged);}, OnDecision=d=>{if(d.Actions.HasFlag(SystemNavigationAction.EnableDockingAssist)){arms++;throw new DockingResetObservationPendingException("immutable-request",20,Now,new IOException("Lost status reply"));}}}; var runner=new SystemNavigationRunner(()=>Player,()=>at);await runner.RunAsync(Target,driver); Check.Equal(NavigationRunState.Completed,runner.State);Check.Equal(1,arms);Check.Equal(1,driver.Calls.Count(x=>x=="apply"));Check.Equal(1,driver.Calls.Count(x=>x=="release")); } [Test] public async Task LostAcknowledgementKeepsTheOriginalTenSecondDeadlineWithoutRearming() { var at=Now;var arms=0;var exits=0;var driver=new Fake{Frame=n=>{at=Now.AddMilliseconds((n-1)*2100);return Landing(n,at,NativeAssistState.Ready);}, OnDecision=d=>{if(d.Actions.HasFlag(SystemNavigationAction.EnableDockingAssist)){arms++;throw new DockingResetObservationPendingException("immutable-request",20,Now,new IOException("Lost status reply"));} if(d.Actions.HasFlag(SystemNavigationAction.ExitForFailedHandoff))exits++;}}; var runner=new SystemNavigationRunner(()=>Player,()=>at);await runner.RunAsync(Target,driver); Check.Equal(NavigationRunState.Stopped,runner.State);Check.That(runner.HandoffFailed);Check.Equal(1,arms);Check.Equal(1,exits); Check.That(runner.Failure is DockingResetObservationPendingException{InnerException:IOException});Check.Equal(6,driver.Calls.Count(x=>x=="read")); } [Test]public async Task ADelayedNineSecondHandoffCompletesWithoutSessionRecoveryOrAnotherRequest() { var at=Now;var arms=0;var exits=0;var driver=new Fake{Frame=n=>{at=Now.AddSeconds(n switch{1=>0,2=>3,3=>9,_=>10.5});return n>=4?Docked with{Sequence=n,ObservedAt=at}:Landing(n,at,n==3?NativeAssistState.Engaged:NativeAssistState.Ready);}, OnDecision=d=>{if(d.Actions.HasFlag(SystemNavigationAction.EnableDockingAssist)){arms++;throw new DockingResetObservationPendingException("immutable-request",20,Now,new IOException());}if(d.Actions.HasFlag(SystemNavigationAction.ExitForFailedHandoff))exits++;}}; var runner=new SystemNavigationRunner(()=>Player,()=>at);await runner.RunAsync(Target,driver); Check.Equal(NavigationRunState.Completed,runner.State);Check.Equal(1,arms);Check.Equal(0,exits); } [Test]public async Task AnUnconfirmedHandoffAtTwoKilometresRecoversBeforeTheTenSecondDeadline() { var at=Now;var driver=new Fake{Frame=n=>{at=Now.AddMilliseconds((n-1)*100);var f=Landing(n,at,NativeAssistState.Ready);return n==1?f:f with{TargetDistance=2000,TargetPosition=new(0,0,2000)};}, OnDecision=d=>{if(d.Actions.HasFlag(SystemNavigationAction.EnableDockingAssist))throw new DockingResetObservationPendingException("immutable-request",20,Now,new IOException());}}; var runner=new SystemNavigationRunner(()=>Player,()=>at);await runner.RunAsync(Target,driver);Check.That(runner.HandoffFailed);Check.Equal(2,driver.Calls.Count(x=>x=="read")); } [Test]public async Task HumanOverrideDuringAnUnconfirmedHandoffDoesNotTriggerMenuRecovery() { var at=Now;var exits=0;var driver=new Fake{Frame=n=>{at=Now.AddSeconds((n-1)*11);return Landing(n,at,NativeAssistState.Ready) with{PlayerOverride=n>1};}, OnDecision=d=>{if(d.Actions.HasFlag(SystemNavigationAction.EnableDockingAssist))throw new DockingResetObservationPendingException("immutable-request",20,Now,new IOException());if(d.Actions.HasFlag(SystemNavigationAction.ExitForFailedHandoff))exits++;}}; var runner=new SystemNavigationRunner(()=>Player,()=>at);await runner.RunAsync(Target,driver);Check.Equal(NavigationRunState.Stopped,runner.State);Check.Equal(0,exits);Check.That(!runner.HandoffFailed); } [Test]public async Task PendingResetKeepsReadingWithoutAlternatingOrRepeatingProgressEveryFrame() { var at=Now;var arms=0;var messages=new List(); var driver=new Fake{Frame=n=>{at=Now.AddMilliseconds((n-1)*100);return n==13?Docked with{Sequence=n,ObservedAt=at}:Landing(n,at,NativeAssistState.Ready);}, OnDecision=d=>{if(d.Actions.HasFlag(SystemNavigationAction.EnableDockingAssist)){arms++;throw new DockingResetObservationPendingException("immutable-request",20,Now,new IOException("Lost status reply"));}}}; var runner=new SystemNavigationRunner(()=>Player,()=>at);runner.Progress+=p=>messages.Add(p.Message); await runner.RunAsync(Target,driver); Check.Equal(NavigationRunState.Completed,runner.State);Check.Equal(1,arms);Check.Equal(13,driver.Calls.Count(x=>x=="read")); Check.Equal(1,messages.Count(x=>x=="正在核对归零回执与自动降落状态,不重发请求。"));Check.That(messages.Count<=6); } [Test] public async Task AResetReceiptForAnotherMarketCannotBecomeAnObservedHandoff() { var driver=new Fake{Frame=_=>Landing(1,Now,NativeAssistState.Ready),OnApply=()=>throw new DockingResetObservationPendingException("wrong-market",99,Now,new IOException())}; var runner=new SystemNavigationRunner(()=>Player,()=>Now);await runner.RunAsync(Target,driver); Check.Equal(NavigationRunState.Failed,runner.State);Check.That(runner.Failure is InvalidDataException);Check.Equal(1,driver.Calls.Count(x=>x=="apply")); } [Test] public async Task WrongSystemOrUnhealthyHullNeverSelectsOrControlsATarget() { foreach(var player in new[]{Player with{Navigation=new(){CurrentSystemAddress=2}},Player with{Ships=[Player.Ships[0] with{HullHealth=0}]}}) { var driver=new Fake();var runner=new SystemNavigationRunner(()=>player,()=>Now);await runner.RunAsync(Target,driver); Check.Equal(NavigationRunState.Failed,runner.State);Check.Equal("release",string.Join(',',driver.Calls)); } } [Test]public async Task StableHistoricalHullDamageDoesNotBlockANewVerifiedLocalNavigation() { var driver=new Fake{Frame=_=>Docked};var runner=new SystemNavigationRunner(()=>Player with{Ships=[Player.Ships[0] with{HullHealth=.49}]},()=>Now); await runner.RunAsync(Target,driver);Check.Equal(NavigationRunState.Completed,runner.State);Check.That(driver.Calls.Contains("select")); } [Test]public async Task ActualThreatDuringNativeLandingIsTypedAndReleasesInputsWithoutAnotherApply() { var current=Player;var driver=new Fake{OnPrepare=()=>current=current with{Navigation=current.Navigation with{ExternalThreat=true}}}; var runner=new SystemNavigationRunner(()=>current,()=>Now);await runner.RunAsync(Target,driver); Check.Equal(NavigationRunState.Failed,runner.State);Check.That(runner.Failure is FlightThreatPauseException); Check.That(!driver.Calls.Contains("apply"));Check.Equal("release",driver.Calls[^1]); } private static SystemFlightFrame AwaitingRequest(int sequence,DateTimeOffset at)=>Docked with{Sequence=sequence,ObservedAt=at,Mode=LocalFlightMode.Normal, TargetPosition=new(0,0,5000),TargetDistance=5000,Assists=new(false,true,false),DockingAssist=NativeAssistState.Ready,Clearance=null}; [Test]public async Task ThreeUnexecutedDockingRequestsTriggerBoundedSessionRecovery() { var at=Now;var requests=0; var driver=new Fake{Frame=n=>{at=Now.AddSeconds(n*11);return AwaitingRequest(n,at);}, OnDecision=d=>{if(d.Actions.HasFlag(SystemNavigationAction.RequestDocking)){requests++;throw new DockingRequestNotAppliedException();}}}; var runner=new SystemNavigationRunner(()=>Player,()=>at);await runner.RunAsync(Target,driver); Check.Equal(3,requests);Check.Equal(NavigationRunState.Failed,runner.State);Check.That(runner.HandoffFailed); Check.That(runner.Failure is DockingHandoffException);Check.Equal("release",driver.Calls[^1]); } [Test]public async Task AcknowledgedRequestsAndRealQueuesDoNotBecomeUnexecutedRequestFailures() { var at=Now;var requests=0; var driver=new Fake{Frame=n=>{at=Now.AddSeconds(n*11);return n<25?AwaitingRequest(n,at):Docked with{Sequence=n,ObservedAt=at};}, OnDecision=d=>{if(d.Actions.HasFlag(SystemNavigationAction.RequestDocking))requests++;}}; var runner=new SystemNavigationRunner(()=>Player,()=>at);await runner.RunAsync(Target,driver); Check.Equal(24,requests);Check.Equal(NavigationRunState.Completed,runner.State);Check.That(!runner.HandoffFailed); } [Test]public async Task AConfirmedRequestResetsOnlyItsConsecutiveFailureCount() { var at=Now;var requests=0; var driver=new Fake{Frame=n=>{at=Now.AddSeconds(n*11);return n<4?AwaitingRequest(n,at):Docked with{Sequence=n,ObservedAt=at};}, OnDecision=d=>{if(d.Actions.HasFlag(SystemNavigationAction.RequestDocking)&&++requests!=2)throw new DockingRequestNotAppliedException();}}; var runner=new SystemNavigationRunner(()=>Player,()=>at);await runner.RunAsync(Target,driver); Check.Equal(3,requests);Check.Equal(NavigationRunState.Completed,runner.State);Check.That(!runner.HandoffFailed); } [Test]public void BernalStationsAreClassifiedWithOtherDockableStarports() =>Check.Equal(SystemLocationCategory.Station,XFEEDToolBox.Core.Native.NativeSystemLocationReader.Category(10)); [Test] public async Task UnsupportedAdapterAndWrongTargetOrBuildCannotSendFlightInput() { foreach(var driver in new[]{new Fake{Ready=false},new Fake{Frame=_=>Docked with{SelectedTarget=Target with{MarketId=99}}},new Fake{Frame=_=>Docked with{GameSha256=new('b',64)}}}) { var runner=new SystemNavigationRunner(()=>Player,()=>Now);await runner.RunAsync(Target,driver); Check.That(runner.State is NavigationRunState.Stopped or NavigationRunState.Failed);Check.That(!driver.Calls.Contains("apply"));Check.Equal("release",driver.Calls[^1]); } } [Test] public async Task TransientTransportFailureRetriesWithoutReplacingTheRequestedTarget() { var driver=new Fake{Frame=n=>n==1?throw new IOException("Transient fixture"):Docked}; var runner=new SystemNavigationRunner(()=>Player,()=>Now);await runner.RunAsync(Target,driver); Check.Equal(NavigationRunState.Completed,runner.State);Check.Equal(1,driver.Calls.Count(x=>x=="select"));Check.Equal(2,driver.Calls.Count(x=>x=="read")); } [Test] public async Task CancellationAfterPreparationSendsNoFlightInputAndReleases() { using var stop=new CancellationTokenSource();var driver=new Fake{OnPrepare=stop.Cancel};var runner=new SystemNavigationRunner(()=>Player,()=>Now); await runner.RunAsync(Target,driver,stop.Token);Check.Equal(NavigationRunState.Stopped,runner.State);Check.That(!driver.Calls.Contains("apply"));Check.Equal("release",driver.Calls[^1]); } [Test] public async Task FreshGeometryCannotResetRecoveryWhileEveryControlRequestIsRefused() { var at=Now; var driver=new Fake{Frame=n=>{at=Now.AddSeconds(n*20);return Docked with{Sequence=n,ObservedAt=at,Mode=LocalFlightMode.Normal,TargetPosition=new(0,0,7000),TargetDistance=7000};}, OnApply=()=>throw new FlightTelemetryUnavailableException("Health fixture is temporarily unavailable")}; var runner=new SystemNavigationRunner(()=>Player,()=>at);await runner.RunAsync(Target,driver); Check.Equal(NavigationRunState.Failed,runner.State);Check.Equal(3,driver.Calls.Count(x=>x=="apply"));Check.Equal("release",driver.Calls[^1]); } [Test] public async Task ConfirmedOriginalSceneTransitionCanReplaceOnlyItsPreviousEntity() { var player=Player with{Navigation=Player.Navigation with{SupercruiseEnteredAt=Now}}; var driver=new Fake{Frame=n=>n==1?Docked with{Mode=LocalFlightMode.Normal,TargetDistance=1000,TargetPosition=new(0,0,1000)} :Docked with{Sequence=2,ShipEntity=18,ReplacesShipEntity=17,SceneTransitionAt=Now}}; var runner=new SystemNavigationRunner(()=>player,()=>Now);await runner.RunAsync(Target,driver); Check.Equal(NavigationRunState.Completed,runner.State);Check.Equal(2,driver.Calls.Count(x=>x=="apply")); } [Test] public async Task SceneTransitionCannotHideWrongEntityOldEventOrChangedLoadout() { var player=Player with{Navigation=Player.Navigation with{SupercruiseEnteredAt=Now}}; foreach(var replacement in new[]{Docked with{Sequence=2,ShipEntity=18,ReplacesShipEntity=16,SceneTransitionAt=Now}, Docked with{Sequence=2,ShipEntity=18,ReplacesShipEntity=17,SceneTransitionAt=Now.AddMinutes(-1)}, Docked with{Sequence=2,ShipEntity=18,ReplacesShipEntity=17,SceneTransitionAt=Now,LoadoutFingerprint="changed"}}) { var driver=new Fake{Frame=n=>n==1?Docked with{Mode=LocalFlightMode.Normal,TargetDistance=1000,TargetPosition=new(0,0,1000)}:replacement}; var runner=new SystemNavigationRunner(()=>player,()=>Now);await runner.RunAsync(Target,driver); Check.Equal(NavigationRunState.Failed,runner.State);Check.Equal(1,driver.Calls.Count(x=>x=="apply"));Check.Equal("release",driver.Calls[^1]); } } [Test]public async Task SceneTransitionUsesTheVerifiedGameClockWithoutAcceptingAnUnexplainedFutureEvent() { foreach(var verified in new[]{false,true}) { var transition=Now.AddSeconds(6.464);var player=Player with{Navigation=Player.Navigation with{SupercruiseEnteredAt=transition}, Status=new(16777232,0,32,1,0,null,null,null,null,[]),StatusAt=transition,StatusReadAt=Now,StatusWrittenAt=verified?Now:null}; var driver=new Fake{Frame=n=>n==1?Docked with{Mode=LocalFlightMode.Normal,TargetDistance=1000,TargetPosition=new(0,0,1000)} :Docked with{Sequence=2,ShipEntity=18,ReplacesShipEntity=17,SceneTransitionAt=transition}}; var runner=new SystemNavigationRunner(()=>player,()=>Now);await runner.RunAsync(Target,driver); Check.Equal(verified?NavigationRunState.Completed:NavigationRunState.Failed,runner.State); } } }