XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEEDToolBox

【WinUI】XFE精英危险工具箱

公开
关注 0 Fork 0 Star 0
UTF-8
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Navigation;
using XFEExtension.NetCore.XUnit.Attributes;

namespace XFEEDToolBox.Test;

[TestFixture]
public sealed class FlightAutomationCoordinatorTests
{
    [Test]public void TypedDepartureRecoveryReachesTheTradeEntryWithoutBeingWrapped()
    {
        var failure=new StationDepartureRecoveryException(4210924803,TimeSpan.FromMinutes(3));
        try{new FlightAutomationResult(NavigationRunState.Failed,failure).RequireCompleted(default);}
        catch(StationDepartureRecoveryException observed){Check.That(ReferenceEquals(failure,observed));return;}
        throw new InvalidOperationException("Departure recovery signal was lost.");
    }
    private const string Build="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
    private static readonly DateTimeOffset Session=DateTimeOffset.UtcNow.AddMinutes(-5);
    private static NavWaypoint Point(long id)=>new(id,"System "+id,new(id,0,0),"K");
    private static PlayerState Initial=>new(){InGame=true,Docked=true,MarketId=10,StationName="Station 10",StationServicesKnown=true,StationServices=["refuel"],CargoReliable=true,
        SystemName="System 1",CommanderId="F1",CurrentShipId=7,LoadGameAt=Session,
        Ships=[new(){ShipId=7,HullHealth=1,Modules=[new(){Item="int_dockingcomputer_advanced",Enabled=true,Health=1}]}],
        Navigation=new(){CurrentSystemAddress=1,TargetSystemAddress=2,Route=[Point(1),Point(2)]}};
    private static readonly Station Destination=new(){MarketId=20,Name="Station 20",SystemName="System 2"};
    private class Journey:IFlightJourneyDriver,ISystemFlightControlDriver,IFlightControlDriver,IFlightJourneyFailureClassifier
    {
        internal PlayerState Player=Initial;
        internal readonly List<string> Calls=[];
        internal Action? OnDepart;
        internal Action? OnReady;
        internal Func<CancellationToken,Task>? Departure;
        internal Func<long?,CancellationToken,Task>? DepartureContinuation;
        internal readonly List<long?> ClearanceMarkets=[];
        internal Action? OnInspect;
        internal bool Arrive=true,AutoLand=true;
        internal bool ManualOverride;
        internal Exception? LocalInspectionFailure,ClassifiedFailure;
        internal Exception? GalaxyReleaseFailure;
        internal int Classifications;
        public Task<Exception> ClassifyFailureAsync(Exception failure,CancellationToken token)
        {token.ThrowIfCancellationRequested();Calls.Add("classify");Classifications++;return Task.FromResult(ClassifiedFailure??failure);}
        public Task EnsureReadyAsync(CancellationToken token){Calls.Add("ready");OnReady?.Invoke();return Task.CompletedTask;}
        public async Task DepartAsync(CancellationToken token)
        {
            Calls.Add("depart");if(Departure is not null)await Departure(token);
            Player=Player with{Docked=false,Navigation=Player.Navigation with{UndockedAt=DateTimeOffset.UtcNow}};OnDepart?.Invoke();
        }
        public async Task EnsureDepartureClearanceAsync(long? localDestinationMarket,CancellationToken token)
        {
            token.ThrowIfCancellationRequested();ClearanceMarkets.Add(localDestinationMarket);
            // This fixture normally represents an inactive original launch;
            // continuation scenarios explicitly supply its bounded observation.
            if(DepartureContinuation is {} continuation){Calls.Add("continue-departure");await continuation(localDestinationMarket,token);}
        }
        public Task<IFlightControlDriver> PlanInterstellarAsync(string destination,CancellationToken token){Calls.Add("plan");return Task.FromResult<IFlightControlDriver>(this);}
        public Task<(SystemNavigationTarget,ISystemFlightControlDriver)> ResolveArrivalAsync(Station destination,CancellationToken token)
        {Calls.Add("resolve");return Task.FromResult<(SystemNavigationTarget,ISystemFlightControlDriver)>((new(Player.Navigation.CurrentSystemAddress!.Value,100,destination.Name,SystemTargetKind.Station,destination.MarketId),this));}
        public Task BeginNavigationSessionAsync(CancellationToken token){Calls.Add("begin");return Task.CompletedTask;}
        public virtual Task<FlightDriverCapabilities> PreflightAsync(CancellationToken token)
        {
            Calls.Add("preflight");Check.That(!Player.Docked,"Jump driver cannot run before confirmed departure.");
            if(Arrive)Player=Player with{SystemName="System 2",Navigation=Player.Navigation with{CurrentSystemAddress=2}};
            return Task.FromResult(new FlightDriverCapabilities(true,Build,true,true,true,true,true,"isolated journey sequence fixture"));
        }
        public virtual Task SelectTargetAsync(NavWaypoint next,CancellationToken token)=>throw new InvalidOperationException("Unexpected live jump input in fixture");
        public virtual Task<FlightObservation> ReadAsync(CancellationToken token)=>throw new InvalidOperationException("Unexpected live flight read in fixture");
        public Task ApplyAsync(FlightDecision decision,long sequence,NavWaypoint next,DateTimeOffset expires,CancellationToken token)=>throw new InvalidOperationException("Unexpected live jump input in fixture");
        public virtual Task ReleaseAsync(){Calls.Add("release-galaxy");if(GalaxyReleaseFailure is {} failure)throw failure;return Task.CompletedTask;}
        public Task<SystemNavigationCapabilities> InspectAsync(CancellationToken token){Calls.Add("inspect");OnInspect?.Invoke();token.ThrowIfCancellationRequested();if(LocalInspectionFailure is {} error)throw error;return Task.FromResult(new SystemNavigationCapabilities(Build,true,true,true,true,true,true,"isolated arrival fixture"));}
        private SystemNavigationTarget? target;
        public Task SelectAsync(SystemNavigationTarget selected,CancellationToken token){Calls.Add("select");target=selected;return Task.CompletedTask;}
        public Task PrepareAsync(CancellationToken token){Calls.Add("prepare");Check.That(!Player.Docked);return Task.CompletedTask;}
        public Task<SystemFlightFrame> ReadSystemFlightAsync(CancellationToken token)
        {
            Calls.Add("read");if(AutoLand)Player=Player with{Docked=true,MarketId=target!.MarketId,StationName=target.Name};
            return Task.FromResult(new SystemFlightFrame{Sequence=1,ObservedAt=DateTimeOffset.UtcNow,GameSha256=Build,LoadoutFingerprint="fit",ShipEntity=17,
                SystemAddress=Player.Navigation.CurrentSystemAddress!.Value,ReferenceWorld=100,SelectedTarget=target,GeometryVerified=true,InGame=true,PlayerOverride=ManualOverride,
                Mode=LocalFlightMode.Docked,DockedMarketId=target!.MarketId,Forward=new(0,0,1),Up=new(0,1,0),MaximumNormalSpeed=100,MaximumCruiseSpeed=1e9,
                BrakingAcceleration=10,TranslationAcceleration=10,LegalApproachSpeed=100});
        }
        public Task ApplySystemFlightAsync(SystemFlightDecision decision,SystemFlightFrame frame,SystemNavigationTarget selected,DateTimeOffset expires,CancellationToken token)
        {Calls.Add("apply-local");return Task.CompletedTask;}
        public Task ReleaseSystemFlightAsync(){Calls.Add("release-local");return Task.CompletedTask;}
    }
    private sealed class FuelJourney:Journey,IFlightFuelPreparationDriver
    {
        internal Func<bool,CancellationToken,Task>? FuelPreparation;
        internal Action? OnFuelReturn;
        internal int Shortages,FuelReturns;
        internal bool FuelMassIncreased;
        public async Task<bool> PrepareDepartureFuelAsync(bool interstellar,CancellationToken token,bool refuel=true)
        {
            Calls.Add(!refuel?"verify-fuel-budget":interstellar?"fuel-interstellar":"fuel-local");token.ThrowIfCancellationRequested();
            if(FuelPreparation is not null)await FuelPreparation(interstellar,token);
            return refuel&&FuelMassIncreased;
        }
        public Task<(SystemNavigationTarget,ISystemFlightControlDriver)> ResolveFuelReturnAsync(Station origin,CancellationToken token)
        {Calls.Add("fuel-return");FuelReturns++;OnFuelReturn?.Invoke();return ResolveArrivalAsync(origin,token);}
        public override Task<FlightDriverCapabilities> PreflightAsync(CancellationToken token)
        {Arrive=Shortages==0;return base.PreflightAsync(token);}
        public override Task SelectTargetAsync(NavWaypoint next,CancellationToken token){Calls.Add("select-galaxy");return Task.CompletedTask;}
        public override Task<FlightObservation> ReadAsync(CancellationToken token)
        {
            Shortages--;return Task.FromResult(new FlightObservation(new(){Sequence=1,ObservedAt=DateTimeOffset.UtcNow,GameSha256=Build,
                LoadoutFingerprint="fit",GeometryVerified=true,SystemAddress=1,TargetSystemAddress=2,PlayerOverride=ManualOverride,
                FuelMain=1,FuelRequiredForJump=1,FuelNeededToNextRefuel=127.99,MinimumFuelToNextRefuel=2,ScoopPowered=false},null));
        }
    }
    [Test]public async Task CrossHostDepartureContinuationFinishesBeforeGalaxyTradeOrLocalDriversCanAcquireFlightInputs()
    {
        foreach(var entry in new[]{"galaxy","trade","local"})
        {
            var driver=new Journey{Player=Initial with{Docked=false,MarketId=null}};var flights=new FlightAutomationCoordinator(()=>driver.Player);
            var entered=new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);var clear=new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
            driver.DepartureContinuation=async(_,token)=>{entered.TrySetResult();await clear.Task.WaitAsync(token);};
            var run=entry=="galaxy"?flights.NavigateGalaxyAsync("System 2",driver,driver)
                :entry=="trade"?flights.TravelToStationAsync(Destination,driver)
                :flights.NavigateLocalAsync(new(1,100,"Station 20",SystemTargetKind.Station,20),driver);
            await entered.Task.WaitAsync(TimeSpan.FromSeconds(2));
            Check.That(!driver.Calls.Any(call=>call is "plan" or "begin" or "preflight" or "select" or "prepare" or "apply-local"),string.Join(',',driver.Calls));
            clear.SetResult();var result=await run.WaitAsync(TimeSpan.FromSeconds(2));Check.Equal(NavigationRunState.Completed,result.State);
            Check.That(driver.ClearanceMarkets.Count>0);Check.That(!driver.Calls.Contains("depart"),"An inherited launch must not send a second launch request.");
        }
    }
    [Test]public async Task CancelledOrUnsafeInheritedDepartureNeverPlansOrAcquiresTheNextFlightPhase()
    {
        foreach(var cancel in new[]{true,false})
        {
            var driver=new Journey{Player=Initial with{Docked=false,MarketId=null}};var flights=new FlightAutomationCoordinator(()=>driver.Player);
            var entered=new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);using var stop=new CancellationTokenSource();
            driver.DepartureContinuation=async(_,token)=>{entered.TrySetResult();if(!cancel)throw new FlightHealthPauseException();await Task.Delay(Timeout.Infinite,token);};
            var run=flights.TravelToStationAsync(Destination,driver,stop.Token);await entered.Task.WaitAsync(TimeSpan.FromSeconds(2));if(cancel)stop.Cancel();
            var result=await run.WaitAsync(TimeSpan.FromSeconds(2));Check.Equal(cancel?NavigationRunState.Stopped:NavigationRunState.Failed,result.State);
            if(!cancel)Check.That(result.Failure is FlightHealthPauseException);
            Check.That(!driver.Calls.Any(call=>call is "plan" or "begin" or "preflight" or "resolve" or "select" or "apply-local"));
        }
    }
    [Test]public async Task NormalSameOriginReturnKeepsItsLocalDestinationAndDoesNotRequireANewUndockReceipt()
    {
        var old=DateTimeOffset.UtcNow.AddMinutes(-1);var driver=new Journey{Player=Initial with{Docked=false,MarketId=null,Navigation=Initial.Navigation with{UndockedAt=old}}};
        driver.DepartureContinuation=(market,_)=>{Check.Equal(10L,market!.Value);return Task.CompletedTask;};
        var flights=new FlightAutomationCoordinator(()=>driver.Player);var result=await flights.NavigateStationAsync(new(){MarketId=10,Name="Station 10",SystemName="System 1"},driver);
        Check.Equal(NavigationRunState.Completed,result.State);Check.Equal(old,driver.Player.Navigation.UndockedAt!.Value);Check.That(!driver.Calls.Contains("depart"));
    }
    [Test]public async Task BothDockedStationEntriesPlanThenConfirmFuelBeforeTheyDepart()
    {
        foreach(var trade in new[]{false,true})
        {
            var driver=new FuelJourney();var flights=new FlightAutomationCoordinator(()=>driver.Player);
            var result=trade?await flights.TravelToStationAsync(Destination,driver):await flights.NavigateStationAsync(Destination,driver);
            Check.Equal(NavigationRunState.Completed,result.State);
            Check.That(driver.Calls.IndexOf("plan")<driver.Calls.IndexOf("fuel-interstellar")&&driver.Calls.IndexOf("fuel-interstellar")<driver.Calls.IndexOf("depart"));
        }
    }
    [Test]public async Task OnlyAConfirmedFuelMassIncreaseReleasesTheOldDriverAndReplansOnceBeforeDeparture()
    {
        foreach(var increased in new[]{false,true})
        {
            var driver=new FuelJourney{FuelMassIncreased=increased};
            var result=await new FlightAutomationCoordinator(()=>driver.Player).NavigateStationAsync(Destination,driver);
            Check.Equal(NavigationRunState.Completed,result.State);
            Check.Equal(increased?2:1,driver.Calls.Count(value=>value=="plan"));
            Check.Equal(1,driver.Calls.Count(value=>value=="fuel-interstellar"));
            Check.Equal(increased?1:0,driver.Calls.Count(value=>value=="verify-fuel-budget"));
            if(increased)
            {
                var secondPlan=driver.Calls.FindLastIndex(value=>value=="plan");
                Check.That(driver.Calls.IndexOf("fuel-interstellar")<driver.Calls.IndexOf("release-galaxy")
                    &&driver.Calls.IndexOf("release-galaxy")<secondPlan&&secondPlan<driver.Calls.IndexOf("verify-fuel-budget")
                    &&driver.Calls.IndexOf("verify-fuel-budget")<driver.Calls.IndexOf("depart"));
            }
        }
    }
    [Test]public async Task FuelRefusalAndUserCancellationCannotLeaveTheDockOrStartJumping()
    {
        foreach(var cancelled in new[]{false,true})
        {
            var driver=new FuelJourney();var flights=new FlightAutomationCoordinator(()=>driver.Player);
            driver.FuelPreparation=(_,token)=>{if(cancelled){flights.Stop();token.ThrowIfCancellationRequested();}throw new NavigationFuelResupplyRequiredException(1,1,2);};
            var result=await flights.NavigateStationAsync(Destination,driver);
            Check.Equal(cancelled?NavigationRunState.Stopped:NavigationRunState.Failed,result.State);
            Check.That(!driver.Calls.Contains("depart")&&!driver.Calls.Contains("begin")&&driver.Calls.Contains("release-galaxy"));
        }
    }
    [Test]public async Task ARealMidflightShortfallReturnsOnceToItsConfirmedOriginAndResumesTheOriginalDestination()
    {
        var driver=new FuelJourney{Shortages=1};var flights=new FlightAutomationCoordinator(()=>driver.Player);
        var result=await flights.NavigateStationAsync(Destination,driver);
        Check.Equal(NavigationRunState.Completed,result.State);Check.Equal(1,driver.FuelReturns);Check.Equal(2,driver.Calls.Count(value=>value=="plan"));
        Check.Equal(2,driver.Calls.Count(value=>value=="fuel-interstellar"));Check.Equal(2,driver.Calls.Count(value=>value=="depart"));
        Check.That(driver.Player.Docked&&driver.Player.MarketId==Destination.MarketId);
        Check.That(driver.Calls.IndexOf("release-galaxy")<driver.Calls.IndexOf("fuel-return"));
    }
    [Test]public async Task RepeatedShortfallUnknownOriginAndManualTakeoverNeverLoopOrGuessAnotherStation()
    {
        foreach(var scenario in new[]{"repeated","no-origin","manual"})
        {
            var driver=new FuelJourney{Shortages=scenario=="repeated"?2:1,ManualOverride=scenario=="manual"};
            if(scenario=="no-origin")driver.Player=driver.Player with{Docked=false,StationName="",MarketId=null};
            var result=await new FlightAutomationCoordinator(()=>driver.Player).NavigateStationAsync(Destination,driver);
            Check.Equal(scenario=="manual"?NavigationRunState.Stopped:NavigationRunState.Failed,result.State);
            Check.Equal(scenario=="repeated"?1:0,driver.FuelReturns);
            if(scenario!="manual")Check.That(result.Failure is NavigationFuelResupplyRequiredException);
        }
    }
    [Test]public async Task FuelRecoveryCannotContinueAfterIdentityCargoOrUserStopChanges()
    {
        foreach(var scenario in new[]{"identity","cargo","stop"})
        {
            var driver=new FuelJourney{Shortages=1};var flights=new FlightAutomationCoordinator(()=>driver.Player);
            driver.OnFuelReturn=()=>{if(scenario=="stop")flights.Stop();else driver.Player=scenario=="identity"
                ?driver.Player with{CurrentShipId=8}:driver.Player with{CargoCount=1,Inventory=[new("water","Water",1)]};};
            var result=await flights.NavigateStationAsync(Destination,driver);
            Check.Equal(scenario=="stop"?NavigationRunState.Stopped:NavigationRunState.Failed,result.State);
            Check.Equal(1,driver.Calls.Count(value=>value=="depart"));Check.That(!driver.Calls.Contains("select"));
        }
    }
    [Test]public async Task GalaxyNavigationAlsoConfirmsDockedFuelBeforeDeparture()
    {
        var driver=new FuelJourney();var result=await new FlightAutomationCoordinator(()=>driver.Player).NavigateGalaxyAsync("System 2",driver,driver);
        Check.Equal(NavigationRunState.Completed,result.State);Check.That(driver.Calls.IndexOf("fuel-interstellar")<driver.Calls.IndexOf("depart"));
    }
    [Test]public async Task DockedGalaxyStartConfirmsDepartureBeforeBeginningTheSharedJumpRunner()
    {
        var driver=new Journey();var flights=new FlightAutomationCoordinator(()=>driver.Player);var phases=new List<FlightAutomationStage>();flights.Progress+=p=>phases.Add(p.Stage);
        var result=await flights.NavigateGalaxyAsync("System 2",driver,driver);
        Check.Equal(NavigationRunState.Completed,result.State);Check.Equal("depart,begin,preflight,release-galaxy",string.Join(',',driver.Calls));
        Check.That(phases.IndexOf(FlightAutomationStage.Departing)<phases.IndexOf(FlightAutomationStage.Interstellar));Check.That(!flights.IsActive);
    }
    [Test]public async Task TradeUsesTheSameDepartureJumpAndArrivalPipelineAndVerifiesTheActualDock()
    {
        var driver=new Journey();var flights=new FlightAutomationCoordinator(()=>driver.Player);
        var result=await flights.TravelToStationAsync(Destination,driver);
        Check.Equal(NavigationRunState.Completed,result.State);Check.That(driver.Player.Docked&&driver.Player.MarketId==20);
        Check.Equal(1,driver.Calls.Count(value=>value=="depart"));Check.Equal(1,driver.Calls.Count(value=>value=="plan"));
        Check.That(driver.Calls.IndexOf("release-galaxy")<driver.Calls.IndexOf("select"));Check.Equal("release-local",driver.Calls[^1]);
    }
    [Test]public async Task DockingAtTheDestinationWhileReadinessRecoversDoesNotLaunchAnotherDeparture()
    {
        var driver=new Journey();driver.Player=driver.Player with{Docked=false,SystemName="System 2",Navigation=driver.Player.Navigation with{CurrentSystemAddress=2}};
        driver.OnReady=()=>driver.Player=driver.Player with{Docked=true,MarketId=20};
        var result=await new FlightAutomationCoordinator(()=>driver.Player).TravelToStationAsync(Destination,driver);
        Check.Equal(NavigationRunState.Completed,result.State);Check.Equal("ready",string.Join(',',driver.Calls));
    }
    [Test]public async Task BothStationEntriesClassifyAFailedLocalLegOnlyAfterItsInputsAreReleased()
    {
        foreach(var trade in new[]{false,true})
        {
            var original=new FlightTelemetryUnavailableException("waiting for actual scene");var classified=new SavingSceneRecoveryRequiredException(original);
            var driver=new Journey{LocalInspectionFailure=original,ClassifiedFailure=classified};var flights=new FlightAutomationCoordinator(()=>driver.Player);
            var result=trade?await flights.TravelToStationAsync(Destination,driver):await flights.NavigateStationAsync(Destination,driver);
            Check.Equal(NavigationRunState.Failed,result.State);Check.That(ReferenceEquals(classified,result.Failure));Check.Equal(1,driver.Classifications);
            Check.That(driver.Calls.IndexOf("release-local")<driver.Calls.IndexOf("classify"));
        }
    }
    [Test]public async Task AManualTakeoverIsNeverPassedToTheSceneFailureClassifier()
    {
        var driver=new Journey{ManualOverride=true,AutoLand=false};
        var result=await new FlightAutomationCoordinator(()=>driver.Player).NavigateStationAsync(Destination,driver);
        Check.Equal(NavigationRunState.Stopped,result.State);Check.Equal(0,driver.Classifications);Check.That(result.Failure is null);
    }
    [Test]public async Task LocalNavigationAutomaticallyDepartsButStayingAtTheCurrentStationDoesNotLaunch()
    {
        var driver=new Journey();var flights=new FlightAutomationCoordinator(()=>driver.Player);
        var target=new SystemNavigationTarget(1,100,"Local station",SystemTargetKind.Station,20);
        var result=await flights.NavigateLocalAsync(target,driver);Check.Equal(NavigationRunState.Completed,result.State);
        Check.Equal(1,driver.Calls.Count(value=>value=="depart"));Check.That(!driver.Calls.Contains("plan"));
        driver.Calls.Clear();result=await flights.NavigateLocalAsync(target,driver);
        Check.Equal(NavigationRunState.Completed,result.State);Check.Equal(0,driver.Calls.Count);
    }
    [Test]public async Task AStationChosenFromTheGalaxyUsesTheFullJourneyWithoutBecomingATradeRun()
    {
        var driver=new Journey();var flights=new FlightAutomationCoordinator(()=>driver.Player);var entries=new List<FlightAutomationEntry>();
        flights.Progress+=progress=>entries.Add(progress.Entry);
        var result=await flights.NavigateStationAsync(Destination,driver);
        Check.Equal(NavigationRunState.Completed,result.State);Check.That(entries.Count>0&&entries.All(entry=>entry==FlightAutomationEntry.Galaxy));
        Check.That(driver.Player.Docked&&driver.Player.MarketId==Destination.MarketId);
        Check.That(driver.Calls.IndexOf("plan")<driver.Calls.IndexOf("depart"));Check.That(driver.Calls.IndexOf("release-galaxy")<driver.Calls.IndexOf("select"));
    }
    [Test]public async Task AnUnconfirmedDepartureCannotStartAnotherPhase()
    {
        var driver=new Journey();driver.OnDepart=()=>driver.Player=driver.Player with{Docked=true};
        var result=await new FlightAutomationCoordinator(()=>driver.Player).NavigateGalaxyAsync("System 2",driver,driver);
        Check.Equal(NavigationRunState.Failed,result.State);Check.Equal("depart,release-galaxy",string.Join(',',driver.Calls));
    }
    [Test]public async Task AnOldUndockReceiptCannotConfirmThisDeparture()
    {
        var driver=new Journey();driver.OnDepart=()=>driver.Player=driver.Player with{Navigation=driver.Player.Navigation with{UndockedAt=Session}};
        var result=await new FlightAutomationCoordinator(()=>driver.Player).NavigateGalaxyAsync("System 2",driver,driver);
        Check.Equal(NavigationRunState.Failed,result.State);Check.Equal("depart,release-galaxy",string.Join(',',driver.Calls));
    }
    [Test]public async Task DifferentEntriesCannotQueueOrStealAnActiveControlSessionAndStopCancelsDeparture()
    {
        var entered=new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        var driver=new Journey{Departure=async token=>{entered.SetResult();await Task.Delay(Timeout.Infinite,token);}};
        var flights=new FlightAutomationCoordinator(()=>driver.Player);var first=flights.NavigateGalaxyAsync("System 2",driver,driver);await entered.Task;
        var rejected=false;
        try{await flights.TravelToStationAsync(Destination,driver);}catch(InvalidOperationException){rejected=true;}
        Check.That(rejected);flights.Stop();var result=await first;
        Check.Equal(NavigationRunState.Stopped,result.State);Check.Equal("depart,release-galaxy",string.Join(',',driver.Calls));Check.That(!flights.IsActive);
    }
    [Test]public async Task HealthProtectionPausePreservesTheExactCauseThroughCancellationAndTradeCompletion()
    {
        var driver=new Journey();var flights=new FlightAutomationCoordinator(()=>driver.Player);var reason=new FlightHealthPauseException();
        driver.Departure=token=>{flights.PauseForSafety(reason);token.ThrowIfCancellationRequested();return Task.CompletedTask;};
        var result=await flights.TravelToStationAsync(Destination,driver);
        Check.Equal(NavigationRunState.Failed,result.State);Check.That(ReferenceEquals(reason,result.Failure));
        try{result.RequireCompleted(default);}catch(FlightHealthPauseException observed){Check.That(ReferenceEquals(reason,observed));return;}
        throw new InvalidOperationException("The recoverable health pause was lost.");
    }
    [Test]public async Task AnInnerRunnerThatConsumesProtectionCancellationStillReturnsItsTypedCauseAfterRelease()
    {
        var driver=new Journey();var flights=new FlightAutomationCoordinator(()=>driver.Player);var reason=new FlightHealthPauseException();
        driver.OnInspect=()=>flights.PauseForSafety(reason);
        var result=await flights.TravelToStationAsync(Destination,driver);
        Check.Equal(NavigationRunState.Failed,result.State);Check.That(ReferenceEquals(reason,result.Failure));
        Check.That(driver.Calls.Contains("release-local"));Check.Equal(0,driver.Classifications);
    }
    [Test]public async Task ExplicitStopOverridesProtectionPauseInEitherOrder()
    {
        foreach(var manualFirst in new[]{false,true})
        {
            var driver=new Journey();var flights=new FlightAutomationCoordinator(()=>driver.Player);
            driver.Departure=token=>
            {
                if(manualFirst)flights.Stop();
                flights.PauseForSafety(new FlightHealthPauseException());
                if(!manualFirst)flights.Stop();
                token.ThrowIfCancellationRequested();return Task.CompletedTask;
            };
            var result=await flights.TravelToStationAsync(Destination,driver);
            Check.Equal(NavigationRunState.Stopped,result.State);Check.That(result.Failure is null);
        }
    }
    [Test]public async Task ExternalCancellationOverridesProtectionAndCannotEnterSafetyRecovery()
    {
        using var external=new CancellationTokenSource();var driver=new Journey();var flights=new FlightAutomationCoordinator(()=>driver.Player);
        driver.Departure=token=>{flights.PauseForSafety(new FlightHealthPauseException());external.Cancel();token.ThrowIfCancellationRequested();return Task.CompletedTask;};
        var result=await flights.TravelToStationAsync(Destination,driver,external.Token);
        Check.Equal(NavigationRunState.Stopped,result.State);Check.That(result.Failure is null);
        try{result.RequireCompleted(external.Token);}catch(OperationCanceledException){return;}
        throw new InvalidOperationException("A cancelled user token must remain cancelled.");
    }
    [Test]public async Task ManualOrExternalCancellationAlsoOverridesAConcurrentTypedDriverFailure()
    {
        foreach(var externalCancel in new[]{false,true})
        {
            using var external=new CancellationTokenSource();var driver=new Journey();var flights=new FlightAutomationCoordinator(()=>driver.Player);
            driver.Departure=token=>
            {
                flights.PauseForSafety(new FlightHealthPauseException());
                if(externalCancel)external.Cancel();else flights.Stop();
                throw new FlightHealthPauseException();
            };
            var result=await flights.TravelToStationAsync(Destination,driver,external.Token);
            Check.Equal(NavigationRunState.Stopped,result.State);Check.That(result.Failure is null);
        }
    }
    [Test]public async Task ExplicitStopAlsoOverridesAnInnerRunnerFailedResult()
    {
        var driver=new Journey();driver.Player=driver.Player with{Docked=false};var flights=new FlightAutomationCoordinator(()=>driver.Player);
        driver.OnInspect=()=>{flights.PauseForSafety(new FlightHealthPauseException());flights.Stop();throw new FlightHealthPauseException();};
        var result=await flights.NavigateLocalAsync(new(1,100,"Local station",SystemTargetKind.Station,20),driver);
        Check.Equal(NavigationRunState.Stopped,result.State);Check.That(result.Failure is null);Check.That(driver.Calls.Contains("release-local"));
    }
    [Test]public async Task ConcurrentManualAndProtectionStopsAlwaysKeepTheManualStop()
    {
        for(var attempt=0;attempt<20;attempt++)
        {
            var driver=new Journey();var flights=new FlightAutomationCoordinator(()=>driver.Player);
            driver.Departure=async token=>
            {
                await Task.WhenAll(Task.Run(()=>flights.PauseForSafety(new FlightHealthPauseException())),Task.Run(flights.Stop));
                token.ThrowIfCancellationRequested();
            };
            var result=await flights.TravelToStationAsync(Destination,driver);
            Check.Equal(NavigationRunState.Stopped,result.State);Check.That(result.Failure is null);
        }
    }
    [Test]public async Task ProtectionReasonsBelongOnlyToTheirActiveJourney()
    {
        var driver=new Journey();var flights=new FlightAutomationCoordinator(()=>driver.Player);var firstReason=new FlightHealthPauseException();
        flights.PauseForSafety(firstReason);
        driver.Departure=token=>{flights.PauseForSafety(firstReason);token.ThrowIfCancellationRequested();return Task.CompletedTask;};
        var first=await flights.TravelToStationAsync(Destination,driver);Check.That(ReferenceEquals(firstReason,first.Failure));
        flights.PauseForSafety(new FlightThreatPauseException());driver.Departure=token=>{flights.Stop();token.ThrowIfCancellationRequested();return Task.CompletedTask;};
        var second=await flights.TravelToStationAsync(Destination,driver);Check.Equal(NavigationRunState.Stopped,second.State);Check.That(second.Failure is null);
        driver.Departure=null;var third=await flights.TravelToStationAsync(Destination,driver);Check.Equal(NavigationRunState.Completed,third.State);Check.That(third.Failure is null);
    }
    [Test]public async Task OnlyASafetyPauseDuringDepartureCarriesTheActualOriginAndReusingItsCauseClearsThatOrigin()
    {
        var driver=new Journey();var flights=new FlightAutomationCoordinator(()=>driver.Player);var reason=new FlightHealthPauseException();
        driver.Departure=token=>{flights.PauseForSafety(reason);token.ThrowIfCancellationRequested();return Task.CompletedTask;};
        var departure=await flights.TravelToStationAsync(Destination,driver);
        Check.That(ReferenceEquals(reason,departure.Failure));Check.Equal(10L,FlightSafetyPauseException.DepartureMarket(reason)!.Value);
        Check.Equal(10L,FlightSafetyPauseException.DepartureMarket(new InvalidOperationException("journey wrapper",new AggregateException(reason)))!.Value);
        driver.Departure=null;driver.Player=driver.Player with{Docked=false};driver.OnInspect=()=>flights.PauseForSafety(reason);
        var local=await flights.TravelToStationAsync(Destination,driver);
        Check.That(ReferenceEquals(reason,local.Failure));Check.That(FlightSafetyPauseException.DepartureMarket(reason) is null);
    }
    [Test]public async Task OnlyAnUndockedDeparturePauseNeedsOneSessionRecoveryAfterTheSafetyWait()
    {
        var driver=new Journey();var flights=new FlightAutomationCoordinator(()=>driver.Player);var reason=new FlightHealthPauseException();
        driver.Departure=token=>{flights.PauseForSafety(reason);token.ThrowIfCancellationRequested();return Task.CompletedTask;};
        var result=await flights.TravelToStationAsync(Destination,driver);var wrapped=new InvalidOperationException("original journey",result.Failure);
        Check.That(FlightSafetyPauseException.RequiresDepartureSessionRecovery(wrapped,docked:false,alreadyRecovered:false));
        Check.That(!FlightSafetyPauseException.RequiresDepartureSessionRecovery(wrapped,docked:true,alreadyRecovered:false));
        Check.That(!FlightSafetyPauseException.RequiresDepartureSessionRecovery(wrapped,docked:false,alreadyRecovered:true));
        Check.That(!FlightSafetyPauseException.RequiresDepartureSessionRecovery(new FlightHealthPauseException(),docked:false,alreadyRecovered:false));
        Check.That(!FlightSafetyPauseException.RequiresDepartureSessionRecovery(new FlightThreatPauseException(),docked:false,alreadyRecovered:false));
        Check.That(!FlightSafetyPauseException.RequiresDepartureSessionRecovery(new OperationCanceledException(),docked:false,alreadyRecovered:false));
    }
    [Test]public async Task CancellationOrIdentityChangeAfterDepartureCannotResumeTheNextLeg()
    {
        foreach(var cancel in new[]{true,false})
        {
            using var stop=new CancellationTokenSource();var driver=new Journey();
            driver.OnDepart=()=>{if(cancel)stop.Cancel();else driver.Player=driver.Player with{CurrentShipId=8};};
            var result=await new FlightAutomationCoordinator(()=>driver.Player).NavigateGalaxyAsync("System 2",driver,driver,stop.Token);
            Check.Equal(cancel?NavigationRunState.Stopped:NavigationRunState.Failed,result.State);Check.Equal("depart,release-galaxy",string.Join(',',driver.Calls));
        }
    }
    [Test]public async Task VerifiedGameClockLeadDoesNotRejectTheActualDepartureReceipt()
    {
        var now=DateTimeOffset.UtcNow;var driver=new Journey();
        driver.Player=driver.Player with{Status=new(16777232,0,32,1,0,null,null,null,null,[]),StatusAt=now.AddSeconds(6.464),StatusReadAt=now,StatusWrittenAt=now};
        driver.OnDepart=()=>driver.Player=driver.Player with{Navigation=driver.Player.Navigation with{UndockedAt=DateTimeOffset.UtcNow.AddSeconds(6.464)}};
        var result=await new FlightAutomationCoordinator(()=>driver.Player).TravelToStationAsync(Destination,driver);
        Check.Equal(NavigationRunState.Completed,result.State);Check.That(driver.Calls.Contains("resolve"));
    }
    [Test]public async Task MissingAutoLaunchModuleAndMismatchedRouteNeverSendLaunchInput()
    {
        foreach(var wrongRoute in new[]{false,true})
        {
            var driver=new Journey();if(!wrongRoute)driver.Player=driver.Player with{Ships=[]};
            var result=await new FlightAutomationCoordinator(()=>driver.Player).NavigateGalaxyAsync(wrongRoute?"System 3":"System 2",driver,driver);
            Check.Equal(NavigationRunState.Failed,result.State);Check.Equal(0,driver.Calls.Count);
        }
    }
    [Test]public async Task MissingAutoLaunchModuleRejectsBothStationJourneysBeforeAnyPlanningOrFuelPayment()
    {
        foreach(var local in new[]{false,true})
        {
            var driver=new FuelJourney{FuelMassIncreased=true};var payments=0;
            driver.Player=driver.Player with{Ships=[]};
            driver.FuelPreparation=(_,_)=>{payments++;return Task.CompletedTask;};
            var destination=local?Destination with{SystemName=driver.Player.SystemName}:Destination;
            var result=await new FlightAutomationCoordinator(()=>driver.Player).NavigateStationAsync(destination,driver);
            Check.Equal(NavigationRunState.Failed,result.State);Check.That(result.Failure is NotSupportedException);
            Check.Equal(0,payments);Check.Equal("ready",string.Join(',',driver.Calls));
            Check.That(driver.Player.Docked&&driver.Player.MarketId==10);
        }
    }
    [Test]public async Task ReachingTheSystemDoesNotConfirmAnUnwitnessedStationLanding()
    {
        var driver=new Journey{AutoLand=false};var result=await new FlightAutomationCoordinator(()=>driver.Player).TravelToStationAsync(Destination,driver);
        Check.Equal(NavigationRunState.Failed,result.State);Check.That(!driver.Player.Docked&&result.Failure!.Message.Contains("实际停靠"));
    }
    [Test]public async Task MonitoringCancellationPreservesTheOriginalInvalidDataSafetyOrTransportCauseAndRecordsBeforeCancel()
    {
        foreach(var cause in new Exception[]{new InvalidDataException("identity observation unavailable"),new FlightHealthPauseException(),new IOException("health observation transport")})
        {
            var driver=new Journey();var flights=new FlightAutomationCoordinator(()=>driver.Player);CancellationToken activeToken=default;var beforeCancel=false;
            flights.StopRecorded+=record=>{if(record.Source=="monitoring-pause")beforeCancel=!activeToken.IsCancellationRequested;};
            driver.Departure=token=>{activeToken=token;flights.PauseForMonitoring(cause);token.ThrowIfCancellationRequested();return Task.CompletedTask;};
            var result=await flights.TravelToStationAsync(Destination,driver);Check.Equal(NavigationRunState.Failed,result.State);Check.That(ReferenceEquals(cause,result.Failure)&&beforeCancel);
            var diagnostic=flights.LastStop!;Check.Equal("monitoring-pause",diagnostic.Source);Check.That(!diagnostic.ManualVeto&&!diagnostic.ParentTokenCancelled&&ReferenceEquals(cause,diagnostic.Cause));
            Check.Equal(FlightAutomationEntry.Trade,diagnostic.Entry!.Value);Check.Equal(FlightAutomationStage.Departing,diagnostic.Stage);Check.Equal(7L,diagnostic.Snapshot!.ShipId!.Value);
            Check.That(!flights.IsActive&&flights.LastStop==diagnostic);
        }
    }
    [Test]public async Task ManualVetoKeepsTheFirstMonitoringEvidenceButNeverEntersRecovery()
    {
        var driver=new Journey();var flights=new FlightAutomationCoordinator(()=>driver.Player);var first=new InvalidDataException("first monitoring failure");
        driver.Departure=token=>{flights.PauseForMonitoring(first);flights.PauseForMonitoring(new IOException("later cleanup"));flights.Stop();token.ThrowIfCancellationRequested();return Task.CompletedTask;};
        var result=await flights.TravelToStationAsync(Destination,driver);Check.Equal(NavigationRunState.Stopped,result.State);Check.That(result.Failure is null);
        var record=flights.LastStop!;Check.Equal("monitoring-pause",record.Source);Check.That(record.ManualVeto&&record.ManualVetoAt is not null&&ReferenceEquals(first,record.Cause));
    }
    [Test]public async Task AMonitoringCauseCannotBeReplacedByTheDriversLaterReleaseFailure()
    {
        var driver=new Journey{GalaxyReleaseFailure=new IOException("cleanup release failed")};var flights=new FlightAutomationCoordinator(()=>driver.Player);var first=new InvalidDataException("first source loss");
        driver.Departure=token=>{flights.PauseForMonitoring(first);token.ThrowIfCancellationRequested();return Task.CompletedTask;};
        var result=await flights.TravelToStationAsync(Destination,driver);Check.Equal(NavigationRunState.Failed,result.State);Check.That(ReferenceEquals(first,result.Failure)&&ReferenceEquals(first,flights.LastStop!.Cause));
    }
    [Test]public async Task SeparateJourneysCannotInheritAnotherRunsStopCauseAndEachCompletedStopRemainsQueryable()
    {
        var driver=new Journey();var flights=new FlightAutomationCoordinator(()=>driver.Player);var first=new IOException("first run");var second=new InvalidDataException("second run");
        driver.Departure=token=>{flights.PauseForMonitoring(first);token.ThrowIfCancellationRequested();return Task.CompletedTask;};
        Check.That(ReferenceEquals(first,(await flights.TravelToStationAsync(Destination,driver)).Failure));var firstRecord=flights.LastStop!;
        driver.Departure=token=>{flights.PauseForMonitoring(second);token.ThrowIfCancellationRequested();return Task.CompletedTask;};
        Check.That(ReferenceEquals(second,(await flights.TravelToStationAsync(Destination,driver)).Failure));var secondRecord=flights.LastStop!;
        Check.That(firstRecord.RunId!=secondRecord.RunId&&ReferenceEquals(first,firstRecord.Cause)&&ReferenceEquals(second,secondRecord.Cause));
        flights.PauseForMonitoring(new IOException("after completion"));Check.That(ReferenceEquals(secondRecord,flights.LastStop));
    }
    [Test]public async Task AParentCancellationVetoDoesNotEraseTheRecordedMonitoringCause()
    {
        using var parent=new CancellationTokenSource();var driver=new Journey();var flights=new FlightAutomationCoordinator(()=>driver.Player);var first=new IOException("monitor before parent");
        driver.Departure=token=>{flights.PauseForMonitoring(first);parent.Cancel();token.ThrowIfCancellationRequested();return Task.CompletedTask;};
        var result=await flights.TravelToStationAsync(Destination,driver,parent.Token);Check.Equal(NavigationRunState.Stopped,result.State);Check.That(result.Failure is null);
        Check.That(flights.LastStop!.ParentTokenCancelled&&ReferenceEquals(first,flights.LastStop.Cause));
    }
    private sealed class LoadingJourney(Func<DateTimeOffset> time,bool failRelease=false):Journey
    {
        internal readonly TaskCompletionSource ReleaseEntered=new(TaskCreationOptions.RunContinuationsAsynchronously);
        internal readonly TaskCompletionSource AllowRelease=new(TaskCreationOptions.RunContinuationsAsynchronously);
        private int releaseCount;
        public override Task<FlightDriverCapabilities> PreflightAsync(CancellationToken token)
        {
            Arrive=false;Player=Player with{Navigation=Player.Navigation with{JumpInProgress=true,JumpTargetSystemAddress=2,JumpTargetSystem="System 2",JumpStartedAt=time()}};
            return base.PreflightAsync(token);
        }
        public override async Task ReleaseAsync()
        {
            Calls.Add("release-galaxy");if(Interlocked.Increment(ref releaseCount)!=1)return;
            ReleaseEntered.TrySetResult();await AllowRelease.Task;if(failRelease)throw new IOException("original release was not acknowledged");
        }
    }
    [Test]public async Task InputFreeLoadingOnlyQualifiesAfterSuccessfulReleaseAndForTheExactJumpWithinItsOriginalDeadline()
    {
        var now=DateTimeOffset.UtcNow;var driver=new LoadingJourney(()=>now);var flights=new FlightAutomationCoordinator(()=>driver.Player,()=>now);
        var waiting=new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        flights.Interstellar.Progress+=p=>{if(p.Message.Contains("跃迁已开始"))waiting.TrySetResult();};
        var run=flights.TravelToStationAsync(Destination,driver);await driver.ReleaseEntered.Task.WaitAsync(TimeSpan.FromSeconds(2));
        Check.That(!flights.IsAwaitingInputFreeJump(driver.Player,now));driver.AllowRelease.SetResult();await waiting.Task.WaitAsync(TimeSpan.FromSeconds(2));
        Check.That(flights.IsAwaitingInputFreeJump(driver.Player,now));Check.That(flights.IsAwaitingInputFreeJump(driver.Player,now.AddSeconds(75)));
        Check.That(!flights.IsAwaitingInputFreeJump(driver.Player,now.AddSeconds(75).AddMilliseconds(1)));
        foreach(var changed in new[]{driver.Player with{CommanderId="other"},driver.Player with{CurrentShipId=8},driver.Player with{LoadGameAt=now},
            driver.Player with{Navigation=driver.Player.Navigation with{JumpInProgress=false}},driver.Player with{Navigation=driver.Player.Navigation with{JumpTargetSystemAddress=3}},
            driver.Player with{Navigation=driver.Player.Navigation with{JumpStartedAt=now.AddSeconds(1)}}})
            Check.That(!flights.IsAwaitingInputFreeJump(changed,now));
        flights.Stop();Check.That(!flights.IsAwaitingInputFreeJump(driver.Player,now));Check.Equal(NavigationRunState.Stopped,(await run.WaitAsync(TimeSpan.FromSeconds(2))).State);
    }
    [Test]public async Task AFailedOriginalReleaseNeverAuthorizesTheLoadingException()
    {
        var now=DateTimeOffset.UtcNow;var driver=new LoadingJourney(()=>now,failRelease:true);var flights=new FlightAutomationCoordinator(()=>driver.Player,()=>now);
        var run=flights.TravelToStationAsync(Destination,driver);await driver.ReleaseEntered.Task.WaitAsync(TimeSpan.FromSeconds(2));
        Check.That(!flights.IsAwaitingInputFreeJump(driver.Player,now));driver.AllowRelease.SetResult();var result=await run.WaitAsync(TimeSpan.FromSeconds(2));
        Check.Equal(NavigationRunState.Failed,result.State);Check.That(!flights.IsAwaitingInputFreeJump(driver.Player,now)&&flights.Interstellar.AwaitingInputFreeJump is null);
    }
}