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<string> 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<EmptyStationFlightProof>? 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<EmptyStationValidationPreflight> InspectDockedAsync(EmptyStationValidationPlan p,CancellationToken t){Calls.Add("inspect");return Task.FromResult(new EmptyStationValidationPreflight(Capabilities,Snapshot()));}
public Task<EmptyStationServiceEvidence> 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<EmptyStationValidationSnapshot> 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<InvalidDataException>(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<InvalidDataException>(()=>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<InvalidDataException>(()=>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<InvalidDataException>(()=>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<InvalidDataException>(()=>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<InvalidDataException>(()=>proof.ObserveOriginalOverchargeReceipt(first with{Enable=false},Start));
}
[Test]public void AtcCannotRunBeforeVerifiedScoStopAndSourceZeroCannotBeFaked()
{
Check.Throws<InvalidOperationException>(()=>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<InvalidDataException>(()=>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<InvalidDataException>(()=>proof.ObserveAi(repeated,later));
}
}
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<string> 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<EmptyStationFlightProof>? 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<EmptyStationValidationPreflight> InspectDockedAsync(EmptyStationValidationPlan p,CancellationToken t){Calls.Add("inspect");return Task.FromResult(new EmptyStationValidationPreflight(Capabilities,Snapshot()));}
public Task<EmptyStationServiceEvidence> 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<EmptyStationValidationSnapshot> 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<InvalidDataException>(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<InvalidDataException>(()=>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<InvalidDataException>(()=>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<InvalidDataException>(()=>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<InvalidDataException>(()=>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<InvalidDataException>(()=>proof.ObserveOriginalOverchargeReceipt(first with{Enable=false},Start));
}
[Test]public void AtcCannotRunBeforeVerifiedScoStopAndSourceZeroCannotBeFaked()
{
Check.Throws<InvalidOperationException>(()=>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<InvalidDataException>(()=>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<InvalidDataException>(()=>proof.ObserveAi(repeated,later));
}
}