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<string> Calls=[];
public Func<int,SystemFlightFrame> Frame=_=>Docked;
public Action? OnPrepare;
public Action? OnApply;
public Action<SystemFlightDecision>? OnDecision;
public bool Ready=true;
private int reads;
public Task<SystemNavigationCapabilities> 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<SystemFlightFrame> 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<string>();
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);
}
}
}
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<string> Calls=[];
public Func<int,SystemFlightFrame> Frame=_=>Docked;
public Action? OnPrepare;
public Action? OnApply;
public Action<SystemFlightDecision>? OnDecision;
public bool Ready=true;
private int reads;
public Task<SystemNavigationCapabilities> 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<SystemFlightFrame> 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<string>();
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);
}
}
}