using System.Text.Json; using System.Text.Json.Nodes; using XFEEDToolBox.Core.Models; using XFEEDToolBox.Core.Session; using XFEExtension.NetCore.XUnit.Attributes; namespace XFEEDToolBox.Test; [TestFixture] public class TradePauseRecoveryTests { [Test]public void NativeRenewUsesTheUnsignedNumericEntityWireContractWithoutPrecisionLoss() { foreach(var entity in new[]{11825915469828UL,9007199254740993UL,ulong.MaxValue}) { var request=JsonSerializer.SerializeToElement(new NativeTradePauseRenewRequest(new string('a',32),new string('b',32),entity),ToolboxSettings.JsonOptions); // This is the exact decoder used by native JsonWire.U64. A JSON // string would throw here even when it contains decimal digits. Check.Equal(JsonValueKind.Number,request.GetProperty("entityId").ValueKind); Check.Equal(entity,request.GetProperty("entityId").GetUInt64()); Check.Equal("trade.pause.renew",request.GetProperty("op").GetString()); Check.Equal(750,request.GetProperty("ttlMilliseconds").GetInt32()); } var oldWire=JsonSerializer.SerializeToElement(new{entityId="11825915469828"}); Check.Throws(()=>oldWire.GetProperty("entityId").GetUInt64()); } private static readonly DateTimeOffset Now=DateTimeOffset.Parse("2026-10-25T12:00:00Z"); private static PlayerState Player=>new(){InGame=true,CommanderId="owner",CurrentShipId=5,LoadGameAt=Now.AddHours(-1)}; private static JsonElement Menu(Action? change=null) { var value=JsonSerializer.SerializeToNode(new{state="Pause",nextState="Pause",menuReady=true,kind=1,categoryName="Pause",menuSequence=12UL, observedAtUnixMilliseconds=Now.ToUnixTimeMilliseconds(),flightTelemetryFresh=false, actions=new[]{new{name="InGame",enabled=true},new{name="ExitOptions",enabled=true}}, tradePauseIdentity=new{verified=true,entityId="42",hullFraction=1d,observedAtUnixMilliseconds=Now.ToUnixTimeMilliseconds(),physicsTelemetryRequired=false}, tradePauseRecovery=new{requestId="",applied=false,failed=false}})!.AsObject(); change?.Invoke(value);return JsonSerializer.SerializeToElement(value); } private sealed class Channel:ITradePauseRecoveryChannel { public JsonElement Value=Menu();public List Calls=[];public Action? AfterRead,AfterRenew;public string? ResumeId; public Task PrepareAsync(CancellationToken token){Calls.Add("prepare");return Task.CompletedTask;} public Task ReadAsync(CancellationToken token){Calls.Add("read");AfterRead?.Invoke();return Task.FromResult(Value);} public Task RenewAsync(string owner,string run,ulong entity,CancellationToken token){Calls.Add("renew");Check.Equal(42UL,entity);AfterRenew?.Invoke();return Task.CompletedTask;} public Task ResumeAsync(string owner,string run,string id,ulong sequence,CancellationToken token){Calls.Add("resume");ResumeId=id;Check.Equal(12UL,sequence);return Task.CompletedTask;} public Task EndAsync(string owner,string run){Calls.Add("end");return Task.CompletedTask;} } [Test]public async Task OnlyARunningScopeCanSubmitThePreciseOriginalPauseResume() { var running=false;var channel=new Channel();var session=new TradePauseRecoverySession(channel,()=>Player,()=>running,()=>Now); await session.StepAsync(default);Check.Equal(0,channel.Calls.Count); running=true;await session.StepAsync(default);Check.Equal(1,channel.Calls.Count(x=>x=="resume")); running=false;await session.StepAsync(default);Check.Equal("end",channel.Calls.Last()); await session.EndAsync();running=true;await session.StepAsync(default);Check.Equal(1,channel.Calls.Count(x=>x=="resume")); } [Test]public async Task StaleFlightTelemetryDoesNotBlockFreshOriginalMenuThreadIdentity() { var channel=new Channel();var session=new TradePauseRecoverySession(channel,()=>Player,()=>true,()=>Now); await session.StepAsync(default);Check.That(channel.Calls.Contains("resume")); var stale=new Channel{Value=Menu(row=>row["tradePauseIdentity"]!["observedAtUnixMilliseconds"]=Now.AddSeconds(-1).ToUnixTimeMilliseconds())}; await new TradePauseRecoverySession(stale,()=>Player,()=>true,()=>Now).StepAsync(default);Check.That(!stale.Calls.Contains("renew")&&!stale.Calls.Contains("resume")); } [Test]public async Task ErrorsRespawnExitAndOrdinaryPanelsCannotBeDismissed() { foreach(var state in new[]{"InGame","MainMenu","ExitOptions","Rebirth","NetworkError","Pause Options","Supercruise"}) { var channel=new Channel{Value=Menu(row=>{row["state"]=state;row["nextState"]=state;})}; await new TradePauseRecoverySession(channel,()=>Player,()=>true,()=>Now).StepAsync(default);Check.That(!channel.Calls.Contains("resume")); } } [Test]public async Task DisabledDuplicateStaleAndWrongCategoryActionsCannotResume() { foreach(var mode in new[]{0,1,2,3,4}) { var channel=new Channel{Value=Menu(row=> { if(mode==0)row["actions"]![0]!["enabled"]=false; else if(mode==1)row["actions"]![1]!["name"]="InGame"; else if(mode==2)row["observedAtUnixMilliseconds"]=Now.AddSeconds(-1).ToUnixTimeMilliseconds(); else if(mode==3)row["kind"]=2; else row["nextState"]="ExitOptions"; })}; await new TradePauseRecoverySession(channel,()=>Player,()=>true,()=>Now).StepAsync(default);Check.That(!channel.Calls.Contains("resume")); } } [Test]public async Task CancellationBetweenObservationAndSubmissionRevokesWithoutAResume() { foreach(var afterRenew in new[]{false,true}) { var running=true;var channel=new Channel(); if(afterRenew)channel.AfterRenew=()=>running=false;else channel.AfterRead=()=>running=false; await new TradePauseRecoverySession(channel,()=>Player,()=>running,()=>Now).StepAsync(default);Check.That(!channel.Calls.Contains("resume")); if(afterRenew)Check.That(channel.Calls.Contains("end")); } } [Test]public async Task OneRequestWaitsForTheRealInGameObservationRatherThanRepeating() { var channel=new Channel();var session=new TradePauseRecoverySession(channel,()=>Player,()=>true,()=>Now); await session.StepAsync(default);await session.StepAsync(default);Check.Equal(1,channel.Calls.Count(x=>x=="resume")); channel.Value=Menu(row=>{row["state"]="InGame";row["nextState"]="InGame";row["tradePauseRecovery"]!["requestId"]=channel.ResumeId;row["tradePauseRecovery"]!["applied"]=true;}); await session.StepAsync(default);Check.Equal(1,session.ConfirmedResumes);Check.Equal(1,channel.Calls.Count(x=>x=="resume")); } [Test]public async Task ShipOrSessionChangeAndCurrentUnhealthyIdentityFailClosed() { var current=Player;var channel=new Channel{Value=Menu(row=>{row["state"]="InGame";row["nextState"]="InGame";})}; var session=new TradePauseRecoverySession(channel,()=>current,()=>true,()=>Now); await session.StepAsync(default);Check.That(channel.Calls.Contains("renew")); current=current with{CurrentShipId=6};channel.Value=Menu(); await session.StepAsync(default);Check.That(channel.Calls.Contains("end")&&!channel.Calls.Contains("resume")); foreach(var mode in new[]{0,1,2}) { var invalid=new Channel{Value=Menu(row=> { if(mode==0)row["tradePauseIdentity"]!["verified"]=false; else if(mode==1)row["tradePauseIdentity"]!["hullFraction"]=0; else row["tradePauseIdentity"]!["entityId"]="0"; })}; await new TradePauseRecoverySession(invalid,()=>Player,()=>true,()=>Now).StepAsync(default); Check.That(!invalid.Calls.Contains("renew")&&!invalid.Calls.Contains("resume")); } } }