using System.Text.Json; using XFEEDToolBox.Core.Execution; using XFEEDToolBox.Core.Models; using XFEEDToolBox.Core.Navigation; using XFEExtension.NetCore.XUnit.Attributes; namespace XFEEDToolBox.Test; [TestFixture] public class EmptyStationValidationTests { private static readonly DateTimeOffset Start=DateTimeOffset.FromUnixTimeMilliseconds(1700000000000); private static readonly string Hash=new('a',64),Owner=new('b',32); private static readonly SystemNavigationTarget Target=new(10,20,"Target",SystemTargetKind.Station,2); private static EmptyStationValidationPlan Plan=>new(Check.Station(1),Check.Station(2),Hash,5,192,5000,4); private static EmptyStationValidationCapabilities Caps=>new(true,true,true,true,true,true,true,false); private static SystemFlightFrame Flight(DateTimeOffset at,LocalFlightMode mode=LocalFlightMode.Supercruise,ulong entity=12,ulong world=24)=>new() {Sequence=1,ObservedAt=at,GameSha256=Hash,ShipEntity=entity,ReferenceWorld=world,SelectedTarget=Target,SystemAddress=10,InGame=true,GeometryVerified=true,Mode=mode, HeatPercent=45,TargetDistance=6000,SupercruiseAssist=NativeAssistState.Disabled,Forward=new(0,0,1),Up=new(0,1,0)}; private static CruiseOverchargeObservation Observation(DateTimeOffset at,CruiseOverchargeState state=CruiseOverchargeState.Active)=>new() {ObservedAt=at,ShipEntity=12,ReferenceWorld=24,GameSha256=Hash,OriginalInterfacesVerified=true,OriginalCanEngage=true,NativeFlightConstraintsVerified=true,NativeGeometryVerified=true,State=state,HeatPercent=45,HeatRisePercentPerSecond=1, FuelMainTonnes=31,FuelCapacityTonnes=32,FuelBurnTonnesPerSecond=.1,FuelRateUpperTonnesPerSecond=.3}; private static OriginalCruiseFuelRange Range(DateTimeOffset at)=>new(12,24,at,true,CruiseFuelRangeScope.OriginalScoConsumptionCurve,.05,.3); private static OriginalOverchargeReceipt Command(DateTimeOffset at,bool enable,long sequence=1,bool original=true)=>new(Guid.NewGuid().ToString("N"),11,111,Hash,Owner,1,12,24,sequence,at,at,at.AddMilliseconds(250),enable,true,original){CommandGeneration=sequence}; private static EmptyStationFlightProof Proof()=>new(11,111,Hash,2,4); private static void CompleteSco(EmptyStationFlightProof proof) { proof.ObserveOriginalOverchargeReceipt(Command(Start,true),Start); for(var index=1;index<=5;index++) {var at=Start.AddMilliseconds(index*20);proof.ObserveCruise(Flight(at),Observation(at),Range(at),false,at);} var stopped=Start.AddMilliseconds(120);proof.ObserveOriginalOverchargeReceipt(Command(stopped,false,20),stopped); var idle=Start.AddMilliseconds(140);proof.ObserveCruise(Flight(idle),Observation(idle,CruiseOverchargeState.Cooling),Range(idle),false,idle); } private static SystemFlightFrame Grant(EmptyStationFlightProof proof) { var at=Start.AddMilliseconds(200);var frame=Flight(at,LocalFlightMode.Normal,13,25) with{Clearance=new(2,DockingPermission.Granted,4,at,at.AddMinutes(1))}; proof.ObserveDockingGrant(frame,false,at);return frame; } private static JsonElement Reset(SystemFlightFrame frame,double throttle=0,bool original=true,bool aiWritten=false) { var at=frame.ObservedAt.AddMilliseconds(10); return JsonSerializer.SerializeToElement(new{applied=true,pending=false,failed=false,expired=false,originalResetCalled=original,entityId=frame.ShipEntity.ToString(),marketId=2, observedAtUnixMilliseconds=at.ToUnixTimeMilliseconds(),accumulatorBefore=0,accumulatorAfter=0,sourceAfter=0,actualThrottleAfter=throttle,controllerThrottleAfter=0, dockingStartRequested=true,originalDockingStartCalled=true,dockingStartAtUnixMilliseconds=at.AddMilliseconds(1).ToUnixTimeMilliseconds(),expiresAtUnixMilliseconds=frame.ObservedAt.AddMilliseconds(250).ToUnixTimeMilliseconds(), gameAccumulatorWrittenBySdk=false,gameOutputWrittenBySdk=false,activeFlagWritten=false,aiWritten}); } private static void Complete(EmptyStationFlightProof proof) { CompleteSco(proof);var frame=Grant(proof);proof.ObserveOriginalDockingReset(Reset(frame),frame,Start.AddMilliseconds(211)); var at=Start.AddMilliseconds(230);proof.ObserveAi(frame with{ObservedAt=at,DockingAssist=NativeAssistState.Engaged,Throttle=.7},at); } private sealed class Backend:IEmptyStationValidationBackend { public readonly List Calls=[]; public EmptyStationValidationCapabilities Capabilities=Caps; public DateTimeOffset At=Start; public bool Docked=true,Unknown,Sca,Healthy=true; public long Market=1,Credits=10000; public int ProcessId=11,Observer=33; public Action? FlightEvidence=Complete; public Action? OnTravel; public Action? OnRecover; public EmptyStationValidationSnapshot Snapshot()=>new(ProcessId,111,Hash,"Commander",5,192,0,Credits,true,Docked,Market,5,Docked,Docked,Healthy,false,Unknown,Sca,false,Observer,222,true,At,At); public Task InspectDockedAsync(EmptyStationValidationPlan p,CancellationToken t){Calls.Add("inspect");return Task.FromResult(new EmptyStationValidationPreflight(Capabilities,Snapshot()));} public Task MaintainAsync(long market,CancellationToken t) {Calls.Add("service:"+market);var opening=Credits;var expense=market==1?10:20;Credits-=expense;return Task.FromResult(new EmptyStationServiceEvidence(market,opening,Credits,expense,true,false));} public Task NavigateAsync(Station destination,EmptyStationFlightProof proof,CancellationToken t) {Calls.Add("navigate");Docked=false;OnTravel?.Invoke();t.ThrowIfCancellationRequested();FlightEvidence?.Invoke(proof);At=Start.AddMilliseconds(250);Docked=true;Market=destination.MarketId;return Task.CompletedTask;} public Task ReadSnapshotAsync(CancellationToken t){Calls.Add("read");return Task.FromResult(Snapshot());} public Task ReleaseNavigationAsync(){Calls.Add("release");return Task.CompletedTask;} public Task RecoverSoloSameProcessAsync(int pid,long ticks,CancellationToken t){Calls.Add("recover");Check.Equal(11,pid);Check.Equal(111L,ticks);OnRecover?.Invoke();return Task.CompletedTask;} } [Test]public async Task MissingNativeCapabilityRefusesWhileDockedBeforeMaintenanceOrNavigation() { foreach(var caps in new[]{Caps with{OriginalScoCommands=false},Caps with{OriginalScoFuelBounds=false},Caps with{OriginalInterdictionEscape=false},Caps with{OriginalThrottleReset=false}, Caps with{OriginalDockingAi=false},Caps with{SameProcessSoloRecovery=false},Caps with{OriginalServerRead=false},Caps with{UsesGameSupercruiseAssist=true}}) { using var temp=new TemporaryDirectory();var backend=new Backend{Capabilities=caps};var result=await new EmptyStationValidationWorkflow(backend,()=>backend.At).RunAsync(Plan,temp.File("job")); Check.That(!result.Passed&&backend.Docked);Check.Equal("inspect",string.Join(',',backend.Calls)); } } [Test]public async Task OneBackendCompletesOnlyWithActualScoZeroAiDockedServicesAndWallet() { using var temp=new TemporaryDirectory();var backend=new Backend();var result=await new EmptyStationValidationWorkflow(backend,()=>backend.At).RunAsync(Plan,temp.File("job")); Check.That(!backend.Capabilities.OriginalScoControlQualified&&backend.Capabilities.AvailableForEmptyTrial); Check.That(result.Passed&&result.ActualDocked&&result.Maintained&&!result.RecoveredSameProcess&&result.Flight.ActiveSamples==5&&!result.Flight.NonzeroResetReproduced); Check.Equal("inspect,service:1,read,navigate,read,service:2,read,release",string.Join(',',backend.Calls));Check.Equal(9970L,result.FinalSnapshot!.Credits); } [Test]public async Task CompletedDockingWithoutScoEvidenceCannotPassAndStillFinishesStationServices() { using var temp=new TemporaryDirectory();var backend=new Backend{FlightEvidence=null};var result=await new EmptyStationValidationWorkflow(backend,()=>backend.At).RunAsync(Plan,temp.File("job")); Check.That(!result.Passed&&result.ActualDocked&&result.Maintained&&!backend.Calls.Contains("recover"));Check.Equal(1,backend.Calls.Count(x=>x=="navigate")); } [Test]public async Task UnknownMoneyOrEnabledScaCannotStartEvenWithAllCapabilities() { foreach(var backend in new[]{new Backend{Unknown=true},new Backend{Sca=true},new Backend{Healthy=false}}) { using var temp=new TemporaryDirectory();var result=await new EmptyStationValidationWorkflow(backend,()=>backend.At).RunAsync(Plan,temp.File("job")); Check.That(!result.Passed);Check.Equal("inspect",string.Join(',',backend.Calls)); } } [Test]public async Task OneOrdinaryFailureUsesSameProcessRecoveryWithoutRepeatingTravel() { using var temp=new TemporaryDirectory();var backend=new Backend{OnTravel=()=>throw new IOException("Original flight failure")}; var result=await new EmptyStationValidationWorkflow(backend,()=>backend.At).RunAsync(Plan,temp.File("job")); Check.That(!result.Passed&&result.RecoveredSameProcess);Check.Equal(1,backend.Calls.Count(x=>x=="navigate"));Check.Equal(1,backend.Calls.Count(x=>x=="recover"));Check.Equal(1,backend.Calls.Count(x=>x=="release")); } [Test]public async Task UserCancellationDoesNotEnterRecoveryOrStartAnotherFlight() { using var temp=new TemporaryDirectory();using var stop=new CancellationTokenSource();var backend=new Backend{OnTravel=stop.Cancel}; var result=await new EmptyStationValidationWorkflow(backend,()=>backend.At).RunAsync(Plan,temp.File("job"),stop.Token); Check.That(!result.Passed&&!result.RecoveredSameProcess&&!backend.Calls.Contains("recover"));Check.Equal(1,backend.Calls.Count(x=>x=="navigate")); } [Test]public async Task AReplacementClientOrObserverCannotBeCalledSameProcessRecovery() { foreach(var replaceObserver in new[]{false,true}) { using var temp=new TemporaryDirectory();var backend=new Backend{OnTravel=()=>throw new IOException("Original flight failure")}; backend.OnRecover=()=>{if(replaceObserver)backend.Observer=34;else backend.ProcessId=12;}; var result=await new EmptyStationValidationWorkflow(backend,()=>backend.At).RunAsync(Plan,temp.File("job")); Check.That(!result.Passed&&!result.RecoveredSameProcess&&result.RecoveryFailure is not null);Check.Equal(1,backend.Calls.Count(x=>x=="navigate")); } } [Test]public async Task PreviousAttemptIsPreservedAndNoBackendIsTouched() { using var temp=new TemporaryDirectory();var path=temp.File("job");Directory.CreateDirectory(path);await File.WriteAllTextAsync(Path.Combine(path,"result.json"),"original");var backend=new Backend(); var result=await new EmptyStationValidationWorkflow(backend,()=>backend.At).RunAsync(Plan,path); Check.That(!result.Passed&&backend.Calls.Count==0);Check.Equal("original",await File.ReadAllTextAsync(Path.Combine(path,"result.json"))); } [Test]public void AcknowledgementsAndStartingStateCannotSubstituteForActualActiveSamples() { var proof=Proof();proof.ObserveOriginalOverchargeReceipt(Command(Start,true),Start); var at=Start.AddMilliseconds(10);proof.ObserveCruise(Flight(at),Observation(at,CruiseOverchargeState.Starting),Range(at),false,at); proof.ObserveOriginalOverchargeReceipt(Command(at.AddMilliseconds(1),false,2),at.AddMilliseconds(1));at=at.AddMilliseconds(2); proof.ObserveCruise(Flight(at),Observation(at,CruiseOverchargeState.Idle),Range(at),false,at); Check.That(!proof.OverchargeCompleted);Check.Throws(proof.RequireComplete); } [Test]public void MissingFuelLowerBoundsOrScaOrUnsafeHeatRefuseEvidence() { var at=Start.AddMilliseconds(20); foreach(var range in new[]{Range(at) with{Verified=false},Range(at) with{Scope=CruiseFuelRangeScope.Unverified},Range(at) with{LowerTonnesPerSecond=double.NaN},Range(at) with{LowerTonnesPerSecond=.4}}) {var proof=Proof();proof.ObserveOriginalOverchargeReceipt(Command(Start,true),Start);Check.Throws(()=>proof.ObserveCruise(Flight(at),Observation(at),range,false,at));} foreach(var pair in new[]{(true,45d),(false,100d)}) {var proof=Proof();proof.ObserveOriginalOverchargeReceipt(Command(Start,true),Start);Check.Throws(()=>proof.ObserveCruise(Flight(at),Observation(at) with{HeatPercent=pair.Item2},Range(at),pair.Item1,at));} var unknown=Proof();unknown.ObserveOriginalOverchargeReceipt(Command(Start,true),Start); Check.Throws(()=>unknown.ObserveCruise(Flight(at),Observation(at),Range(at),null,at)); } [Test]public void WrongExpiredOrRenewalOnlyCommandsCannotEstablishOriginalControl() { foreach(var command in new[]{Command(Start,true) with{ProcessId=99},Command(Start,true) with{ProcessStartedUtcTicks=112},Command(Start,true) with{AppliedAt=Start.AddMilliseconds(251)},Command(Start,true,original:false)}) Check.Throws(()=>Proof().ObserveOriginalOverchargeReceipt(command,command.AppliedAt>Start.AddMilliseconds(250)?command.AppliedAt:Start.AddMilliseconds(10))); var proof=Proof();var first=Command(Start,true);proof.ObserveOriginalOverchargeReceipt(first,Start);proof.ObserveOriginalOverchargeReceipt(first,Start); Check.Equal(1,proof.Snapshot().OriginalCommands.Count);Check.Throws(()=>proof.ObserveOriginalOverchargeReceipt(first with{Enable=false},Start)); } [Test]public void AtcCannotRunBeforeVerifiedScoStopAndSourceZeroCannotBeFaked() { Check.Throws(()=>Grant(Proof())); foreach(var invalid in new[]{0,1,2}) { var proof=Proof();CompleteSco(proof);var frame=Grant(proof); var receipt=Reset(frame,throttle:invalid==0?.75:0,original:invalid!=1,aiWritten:invalid==2); Check.Throws(()=>proof.ObserveOriginalDockingReset(receipt,frame,Start.AddMilliseconds(211))); } } [Test]public void ReobservingGrantCannotExtendTwoSecondAiWindow() { var proof=Proof();CompleteSco(proof);var frame=Grant(proof);proof.ObserveOriginalDockingReset(Reset(frame),frame,Start.AddMilliseconds(211)); var later=Start.AddMilliseconds(2201);var repeated=frame with{ObservedAt=later,DockingAssist=NativeAssistState.Engaged,Clearance=frame.Clearance! with{ObservedAt=later}}; proof.ObserveDockingGrant(repeated,false,later);Check.Throws(()=>proof.ObserveAi(repeated,later)); } }