using System.Text.Json; using System.Text.Json.Nodes; using XFEEDToolBox.Core.Navigation; using XFEExtension.NetCore.XUnit.Attributes; namespace XFEEDToolBox.Test; [TestFixture] public sealed class ScoTrialFallbackContinuationTests { private static readonly DateTimeOffset Now=DateTimeOffset.FromUnixTimeMilliseconds(2000); private static readonly ScoTrialContinuationContext Context=new(42,new('a',64),new('b',64),10,20,1,3,100,10000,0); private static JsonObject Status() { var choice=new ScoExitApproachDecision{Entity=10,World=20,TargetKey=3,At=1899,TrialExpiresAt=10000,SourceSpeed=20*299792458d, Mode="trial",Reason=ScoExitApproachDecision.OutsideCalibrationReason,UseEstimatedStop=false}; return new() { ["enabled"]=true,["processId"]=42,["service"]=CruiseControlProtocol.Service,["gameSha256"]=Context.GameSha256,["sampleReady"]=true,["owned"]=false, ["sample"]=new JsonObject{["verified"]=true,["inSupercruise"]=true,["rawFsdState"]=2,["state"]=0,["entityId"]="10",["referenceWorldAddress"]="20", ["observedAtUnixMilliseconds"]=2000,["originalUpdateClockEpoch"]=1,["originalUpdateCompletedSteps"]=160,["originalUpdateClockValid"]=true,["playerOverride"]=false,["healthEmergency"]=false, ["target"]=new JsonObject{["key"]=3}}, ["scoExitEstimate"]=new JsonObject{["mode"]="bounded-trial",["trialExpiresAtUnixMilliseconds"]=10000,["maximumTrialBurstsRemaining"]=1, ["trialBurstActive"]=false,["proofAvailable"]=true,["currentSourceDomainVerified"]=true,["currentParameterBindingVerified"]=true, ["trustedLoadoutFingerprint"]=Context.LoadoutFingerprint,["lastPolicyRevocationFailure"]=null, ["lastExitReceipt"]=new JsonObject{["pass"]=true,["trial"]=false,["production"]=false,["reason"]="same-binding-shadow-completed", ["entityId"]="10",["referenceWorldAddress"]="20",["clockEpoch"]=1,["targetKey"]=3,["startCompletedSteps"]=120,["endCompletedSteps"]=160,["integratedPathDistanceMetres"]=1000}}, ["lastOriginalStop"]=new JsonObject{["nativeMethodCalled"]=true,["argument"]=false,["nativeMethodRva"]="2eaacd0",["reason"]="approach-or-alignment", ["stateBefore"]=2,["stateAfter"]=3,["entityId"]="10",["referenceWorldAddress"]="20",["observedAtUnixMilliseconds"]=1900, ["gate"]=new JsonObject{["identityVerified"]=true,["targetVerified"]=true,["geometryVerified"]=true,["emergency"]=false,["collisionCourse"]=false,["assistEnabled"]=false, ["aimErrorDegrees"]=0,["cruiseParametersVerified"]=true,["cruiseApproachSeconds"]=6,["distanceMetres"]=240*299792458d,["speedMetresPerSecond"]=20*299792458d, ["heatPercent"]=30,["thermalForecast"]=new JsonObject{["known"]=true,["shouldStop"]=false},["currentFrameGrossHeatBoundKnown"]=true,["currentFrameGrossHeatPeakPercent"]=31, ["mainFuelTonnes"]=32,["fuelCapacityTonnes"]=128,["requiredFuelReserveTonnes"]=8,["fuelRateUpperTonnesPerSecond"]=1,["scoExitApproach"]=choice.Json()}} }; } private static bool Allowed(JsonObject status,ScoTrialContinuationContext? context=null) =>ScoTrialFallbackContinuation.CanContinue(JsonSerializer.SerializeToElement(status),context??Context,Now,out _); [Test]public void OneNewCompleteDomainFallbackMayUseOnlyTheAlreadyRemainingBurst() { Check.That(Allowed(Status()));Check.That(!Allowed(Status(),Context with{CompletedFallbacks=1})); Check.That(!Allowed(Status(),Context with{ConsumedReceiptSteps=160}));Check.That(!Allowed(Status(),Context with{OriginalTrialDeadline=10001})); Check.That(!Allowed(Status(),Context with{OriginalTrialDeadline=2000})); } [Test]public void HeatAlignmentNonDistanceAndUnknownStopsCannotCountAsDomainFallbacks() { foreach(var bad in new[]{"heat","alignment","distance","unclassified","estimated","non-native","wrong-state","low-fuel","thermal-unknown"}) { var node=Status();var stop=node["lastOriginalStop"]!;var gate=stop["gate"]!; if(bad=="heat")gate["thermalForecast"]!["shouldStop"]=true; else if(bad=="alignment")gate["aimErrorDegrees"]=7; else if(bad=="distance")gate["distanceMetres"]=300*299792458d; else if(bad=="unclassified")gate["scoExitApproach"]!["reason"]="bounded-trial-armed"; else if(bad=="estimated")gate["scoExitApproach"]!["useEstimatedStop"]=true; else if(bad=="non-native")stop["nativeMethodCalled"]=false; else if(bad=="wrong-state")stop["stateAfter"]=0; else if(bad=="low-fuel")gate["mainFuelTonnes"]=8; else gate.AsObject().Remove("thermalForecast"); Check.That(!Allowed(node)); } } [Test]public void TheOriginalAuthorizationSourceAndBindingMustStillBeCurrent() { foreach(var bad in new[]{"deadline","no-burst","extra-bursts","revoked","unknown-revocation","source","world","epoch","manual","old-sample","foreign-fit"}) { var node=Status();var policy=node["scoExitEstimate"]!;var sample=node["sample"]!; if(bad=="deadline")policy["trialExpiresAtUnixMilliseconds"]=10001; else if(bad=="no-burst")policy["maximumTrialBurstsRemaining"]=0; else if(bad=="extra-bursts")policy["maximumTrialBurstsRemaining"]=2; else if(bad=="revoked")policy["lastPolicyRevocationFailure"]=new JsonObject{["reason"]="source"}; else if(bad=="unknown-revocation")policy.AsObject().Remove("lastPolicyRevocationFailure"); else if(bad=="source")policy["currentSourceDomainVerified"]=false; else if(bad=="world")sample["referenceWorldAddress"]="21"; else if(bad=="epoch")sample["originalUpdateClockEpoch"]=2; else if(bad=="manual")sample["playerOverride"]=true; else if(bad=="old-sample")sample["observedAtUnixMilliseconds"]=1799; else policy["trustedLoadoutFingerprint"]=new string('c',64); Check.That(!Allowed(node)); } } [Test]public void FailedForeignOrAlreadyConsumedReceiptsCannotContinueTheTrial() { foreach(var bad in new[]{"failed","trial","production","old-start","bad-end","foreign-target","foreign-stop","old-choice"}) { var node=Status();var receipt=node["scoExitEstimate"]!["lastExitReceipt"]!; if(bad=="failed")receipt["pass"]=false; else if(bad=="trial")receipt["trial"]=true; else if(bad=="production")receipt["production"]=true; else if(bad=="old-start")receipt["startCompletedSteps"]=100; else if(bad=="bad-end")receipt["endCompletedSteps"]=120; else if(bad=="foreign-target")receipt["targetKey"]=4; else if(bad=="foreign-stop")node["lastOriginalStop"]!["entityId"]="11"; else node["lastOriginalStop"]!["gate"]!["scoExitApproach"]!["observedAtUnixMilliseconds"]=1600; Check.That(!Allowed(node)); } } }