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

XFEEDToolBox

【WinUI】XFE精英危险工具箱

公开
关注 0 Fork 0 Star 0
UTF-8
using System.Diagnostics;
using System.IO.Pipes;
using System.Text;
using System.Text.Json;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Navigation;
using XFEEDToolBox.Core.Plugins;
using XFEExtension.NetCore.XUnit.Attributes;

namespace XFEEDToolBox.Test;

[TestFixture]
public class NativeNavigationTests
{
    private static async Task Serve(string name,object response,CancellationToken token,Action<string>? request=null)
    {
        await using var server=new NamedPipeServerStream(name,PipeDirection.InOut,1,PipeTransmissionMode.Byte,PipeOptions.Asynchronous);
        await server.WaitForConnectionAsync(token);
        var buffer=new byte[4096];var count=await server.ReadAsync(buffer,token);
        request?.Invoke(Encoding.UTF8.GetString(buffer,0,count));
        await server.WriteAsync(JsonSerializer.SerializeToUtf8Bytes(response,ToolboxSettings.JsonOptions),token);
        await server.WriteAsync(new byte[]{(byte)'\n'},token);
        server.WaitForPipeDrain();
    }
    private static async Task Rejects<T>(Func<Task> operation) where T:Exception
    {try{await operation();}catch(T){return;}throw new InvalidOperationException("Expected "+typeof(T).Name);}

    [Test] public async Task StatusCannotActivateNativeHooksAndObservationRequiresSeparateCommand()
    {
        var id=Random.Shared.Next(20000000,29000000);using var timeout=new CancellationTokenSource(TimeSpan.FromSeconds(5));
        var commands=new List<string>();
        var server=Task.Run(async()=>
        {
            await Serve($"XFEEDToolBox.Plugin.{id}",new{schema=1,name=GamePluginManager.PluginId,processId=id,loaded=true,active=true,observationStarted=false},timeout.Token,commands.Add);
            await Serve($"XFEEDToolBox.Plugin.{id}",new{schema=1,name=GamePluginManager.PluginId,processId=id,loaded=true,active=true,observationStarted=true},timeout.Token,commands.Add);
        });
        Check.That(!(await GamePluginManager.ProbeAsync(id,token:timeout.Token)).ObservationStarted);
        Check.That((await GamePluginManager.StartNavigationObservationAsync(id,timeout.Token)).ObservationStarted);
        await server;Check.Equal("status\n",commands[0]);Check.Equal("navigation.observe.start\n",commands[1]);
    }

    [Test] public async Task IncompleteNativeCapabilitiesCannotReadOrWriteFlightControls()
    {
        var id=Random.Shared.Next(30000000,39000000);var hash=new string('a',64);
        using var timeout=new CancellationTokenSource(TimeSpan.FromSeconds(5));
        var server=Serve($"XFEEDToolBox.Navigation.{id}",new{ok=true,result=new{schema=1,name="XFEEDToolBox.Navigation",processId=id,gameSha256=hash,verified=false,continuousPose=true,continuousHeat=true,evidence="Observation only"}},timeout.Token);
        var driver=new NativeFlightControlDriver(id,hash);
        Check.That(!(await driver.PreflightAsync(timeout.Token)).Ready);await server;
        await Rejects<InvalidOperationException>(async()=>{await driver.ReadAsync(timeout.Token);});
        await driver.ReleaseAsync();
    }

    [Test] public async Task BuildMismatchIsRejectedBeforeAnyControlOperation()
    {
        var id=Random.Shared.Next(40000000,49000000);
        using var timeout=new CancellationTokenSource(TimeSpan.FromSeconds(5));
        var server=Serve($"XFEEDToolBox.Navigation.{id}",new{ok=true,result=new{schema=1,name="XFEEDToolBox.Navigation",processId=id,gameSha256=new string('b',64)}},timeout.Token);
        var driver=new NativeFlightControlDriver(id,new string('a',64));
        await Rejects<InvalidDataException>(async()=>{await driver.PreflightAsync(timeout.Token);});await server;
        await Rejects<InvalidDataException>(()=>NavigationRecovery.PreflightWithRecoveryAsync(driver,null,timeout.Token));
        await Rejects<InvalidOperationException>(async()=>{await driver.ReadAsync(timeout.Token);});
        Check.That(!NavigationRecovery.CanRetry(new InvalidOperationException("原生导航进程、构建或协议不匹配。"),default));
    }

    [Test] public async Task HungNativeReplyTimesOutAndReleasesClientForNextConnection()
    {
        var id=Random.Shared.Next(50000000,59000000);var name=$"XFEEDToolBox.Navigation.{id}";
        using var timeout=new CancellationTokenSource(TimeSpan.FromSeconds(5));
        var server=Task.Run(async()=>
        {
            await using(var blocked=new NamedPipeServerStream(name,PipeDirection.InOut,1,PipeTransmissionMode.Byte,PipeOptions.Asynchronous))
            {await blocked.WaitForConnectionAsync(timeout.Token);await blocked.ReadAsync(new byte[256],timeout.Token);await Task.Delay(750,timeout.Token);}
            await Serve(name,new{ok=true,result=new{recovered=true}},timeout.Token);
        });
        var client=new NavigationPipeClient(id);var clock=Stopwatch.StartNew();
        await Rejects<OperationCanceledException>(async()=>{await client.CallAsync(new{op="describe"},timeout.Token);});
        Check.That(clock.Elapsed<TimeSpan.FromSeconds(2));
        await Task.Delay(300,timeout.Token);
        Check.That((await client.CallAsync(new{op="describe"},timeout.Token)).GetProperty("recovered").GetBoolean());
        await server;
    }

    [Test] public async Task ReleaseWaitsForGameThreadCancellationAndKeepsTheSameOwner()
    {
        var id=Random.Shared.Next(60000000,69000000);var hash=new string('a',64);var name=$"XFEEDToolBox.Navigation.{id}";
        using var timeout=new CancellationTokenSource(TimeSpan.FromSeconds(6));
        var requests=new List<JsonElement>();
        void Remember(string text){using var document=JsonDocument.Parse(text);requests.Add(document.RootElement.Clone());}
        var server=Task.Run(async()=>
        {
            await Serve(name,new{ok=true,result=new{schema=1,name="XFEEDToolBox.Navigation",processId=id,gameSha256=hash,verified=true,
                continuousPose=true,continuousHeat=true,starEnvelopes=true,nativeSteering=true,chargeAndCommitInterlock=true,applicationSteeringAxes=true,evidence="Isolated IPC test"}},timeout.Token,Remember);
            await Serve(name,new{ok=true,result=new{accepted=true}},timeout.Token,Remember);
            await Serve(name,new{ok=true,result=new{released=false,cancellationPending=true}},timeout.Token,Remember);
            await Serve(name,new{ok=true,result=new{released=true,cancellationPending=false}},timeout.Token,Remember);
        });
        var driver=new NativeFlightControlDriver(id,hash);
        Check.That((await driver.PreflightAsync(timeout.Token)).Ready);
        await driver.ApplyAsync(new("Test",null,0,false,true,false,null,"Cancellation"),1,
            new(123,"Test",new(0,0,0),"G"),DateTimeOffset.UtcNow.AddMilliseconds(250),timeout.Token);
        await driver.ReleaseAsync();await server;
        await driver.ReleaseAsync();
        Check.Equal(4,requests.Count);
        Check.Equal("release",requests[2].GetProperty("op").GetString()!);
        Check.Equal("release",requests[3].GetProperty("op").GetString()!);
        Check.Equal(requests[1].GetProperty("owner").GetString()!,requests[2].GetProperty("owner").GetString()!);
        Check.Equal(requests[2].GetProperty("owner").GetString()!,requests[3].GetProperty("owner").GetString()!);
    }
    [Test] public async Task RoutePlanningUsesOriginalGameRequestAndPreserves64BitAddressesWithoutFlightInput()
    {
        var id=Random.Shared.Next(80000000,89000000);var hash=new string('a',64);var name=$"XFEEDToolBox.Navigation.{id}";
        const long target=9007199254740995;using var timeout=new CancellationTokenSource(TimeSpan.FromSeconds(5));
        var requests=new List<JsonElement>();void Remember(string text){using var document=JsonDocument.Parse(text);requests.Add(document.RootElement.Clone());}
        var server=Task.Run(async()=>
        {
            await Serve(name,new{ok=true,result=new{schema=1,name="XFEEDToolBox.Navigation",processId=id,gameSha256=hash,verified=true,
                continuousPose=true,continuousHeat=true,starEnvelopes=true,nativeSteering=true,chargeAndCommitInterlock=true,gameRoutePlanning=true,evidence="Isolated route IPC fixture"}},timeout.Token,Remember);
            await Serve(name,new{ok=true,result=new{generation=71,applied=false,pending=true}},timeout.Token,Remember);
            await Serve(name,new{ok=true,result=new{generation=71,applied=true,pending=false,failed=false,destinationAddress="9007199254740995",targetAddress="123",targetFresh=true,topologyFresh=true,waypointCount=2,routeFingerprint="18446744073709551615"}},timeout.Token,Remember);
        });
        var driver=new NativeFlightControlDriver(id,hash);await driver.PreflightAsync(timeout.Token);
        var generation=await driver.RequestGameRouteAsync(new(target,"Destination",new(1,0,0),"G"),timeout.Token);
        var status=await driver.ReadGameRouteStatusAsync(generation,target,timeout.Token);
        await driver.ReleaseAsync();await server;
        Check.Equal(3,requests.Count);Check.Equal("route.plan",requests[1].GetProperty("op").GetString()!);
        Check.Equal(target,requests[1].GetProperty("destinationSystemAddress").GetInt64());Check.Equal(71L,generation);
        Check.Equal("route.status",requests[2].GetProperty("op").GetString()!);Check.Equal(target,status.DestinationAddress);
        Check.Equal(ulong.MaxValue,status.RouteFingerprint);
        Check.Equal(requests[1].GetProperty("owner").GetString()!,requests[2].GetProperty("owner").GetString()!);
    }
    [Test] public async Task BankedAxesUseTheExistingGuardedFlightLeaseAndRequireAnExplicitCapability()
    {
        foreach(var supported in new[]{true,false})
        {
            var id=Random.Shared.Next(100000000,190000000);var hash=new string('a',64);var name=$"XFEEDToolBox.Navigation.{id}";
            using var timeout=new CancellationTokenSource(TimeSpan.FromSeconds(5));var requests=new List<JsonElement>();
            void Remember(string text){using var doc=JsonDocument.Parse(text);requests.Add(doc.RootElement.Clone());}
            var frame=new FlightFrame{Sequence=1,GameSha256=hash,ObservedAt=DateTimeOffset.UtcNow,GeometryVerified=true,AdaptiveFlightControl=true,
                SystemAddress=1,TargetSystemAddress=2,Forward=new(0,0,1),Up=new(0,1,0)};
            var server=Task.Run(async()=>
            {
                await Serve(name,new{ok=true,result=new{schema=1,name="XFEEDToolBox.Navigation",processId=id,gameSha256=hash,verified=true,
                    continuousPose=true,continuousHeat=true,starEnvelopes=true,nativeSteering=true,chargeAndCommitInterlock=true,applicationSteeringAxes=supported,evidence="Isolated three-axis IPC fixture"}},timeout.Token,Remember);
                await Serve(name,new{ok=true,result=new{frame}},timeout.Token,Remember);
                await Serve(name,new{ok=true,result=new{generation=1,selected=true,targetAddress="2"}},timeout.Token,Remember);
                if(supported)await Serve(name,new{ok=true,result=new{accepted=true,steering=new[]{0d,0,-.125},throttleCommand=0}},timeout.Token,Remember);
                await Serve(name,new{ok=true,result=new{released=true}},timeout.Token,Remember);
            });
            var driver=new NativeFlightControlDriver(id,hash);await driver.PreflightAsync(timeout.Token);await driver.ReadAsync(timeout.Token);
            var next=new NavWaypoint(2,"Next",new(1,0,0),"K");await driver.SelectTargetAsync(next,timeout.Token);
            var orbit=new FlightDecision("Orbit",frame.Forward,0,false,false,false,null,"Banked orbit"){UpDirection=new(1,0,0)};
            if(supported)
            {
                await driver.ApplyAsync(orbit,1,next,DateTimeOffset.UtcNow.AddMilliseconds(250),timeout.Token);
                Check.Equal(-.125,driver.LastAppliedControl!.Roll!.Value);
                var input=requests.Single(r=>r.GetProperty("op").GetString()=="apply");var axes=input.GetProperty("steeringAxes");
                Check.Equal(3,axes.GetArrayLength());Check.That(axes[2].GetDouble()<0&&Math.Abs(axes[2].GetDouble())<=.1,"An unmeasured ship starts with conservative roll authority.");
            }
            else await Rejects<InvalidOperationException>(()=>driver.ApplyAsync(orbit,1,next,DateTimeOffset.UtcNow.AddMilliseconds(250),timeout.Token));
            await driver.ReleaseAsync();await server;
            Check.That(requests.All(r=>!r.GetProperty("op").GetString()!.StartsWith("calibrate.")));
        }
    }
    [Test] public async Task RoutePlanningWaitsThroughArrivalCooldownWithoutFlightInputsButRejectsActiveCharging()
    {
        foreach(var (state,owned,fresh,retry) in new[]{(1,false,true,true),(4,false,true,true),(3,false,true,false),(1,true,true,false),(1,false,false,false)})
        {
            var id=Random.Shared.Next(100000000,190000000);var hash=new string('a',64);var name=$"XFEEDToolBox.Navigation.{id}";
            using var timeout=new CancellationTokenSource(TimeSpan.FromSeconds(5));var requests=new List<JsonElement>();
            void Remember(string text){using var doc=JsonDocument.Parse(text);requests.Add(doc.RootElement.Clone());}
            var server=Task.Run(async()=>
            {
                await Serve(name,new{ok=true,result=new{schema=1,name="XFEEDToolBox.Navigation",processId=id,gameSha256=hash,verified=true,
                    continuousPose=true,continuousHeat=true,starEnvelopes=true,nativeSteering=true,chargeAndCommitInterlock=true,gameRoutePlanning=true,evidence="Isolated cooldown route fixture"}},timeout.Token,Remember);
                await Serve(name,new{ok=false,error="Game route planning requires a fresh idle FSD"},timeout.Token,Remember);
                await Serve(name,new{ok=true,result=new{fresh,ownedCharging=owned,state}},timeout.Token,Remember);
                if(retry)await Serve(name,new{ok=true,result=new{generation=72,applied=false,pending=true}},timeout.Token,Remember);
            });
            var driver=new NativeFlightControlDriver(id,hash,()=>new(){InGame=true,Navigation=new(){LastJumpAt=DateTimeOffset.UtcNow.AddSeconds(2)}});
            await driver.PreflightAsync(timeout.Token);var destination=new NavWaypoint(2,"Destination",new(1,0,0),"G");
            if(retry)Check.Equal(72L,await driver.RequestGameRouteAsync(destination,timeout.Token));
            else await Rejects<InvalidOperationException>(()=>driver.RequestGameRouteAsync(destination,timeout.Token));
            await driver.ReleaseAsync();await server;
            Check.Equal(retry?4:3,requests.Count);
            Check.That(requests.All(r=>r.GetProperty("op").GetString() is "describe" or "route.plan" or "fsd.describe"));
            if(retry)Check.Equal(requests[1].GetProperty("owner").GetString()!,requests[3].GetProperty("owner").GetString()!);
        }
    }
    [Test] public async Task DestinationIsAcknowledgedBeforeJumpAndFullThrottleIsRequired()
    {
        var id=Random.Shared.Next(70000000,79000000);var hash=new string('a',64);var name=$"XFEEDToolBox.Navigation.{id}";
        const long target=9007199254740995;using var timeout=new CancellationTokenSource(TimeSpan.FromSeconds(6));
        var requests=new List<JsonElement>();
        void Remember(string text){using var document=JsonDocument.Parse(text);requests.Add(document.RootElement.Clone());}
        var server=Task.Run(async()=>
        {
            await Serve(name,new{ok=true,result=new{schema=1,name="XFEEDToolBox.Navigation",processId=id,gameSha256=hash,verified=true,
                continuousPose=true,continuousHeat=true,starEnvelopes=true,nativeSteering=true,chargeAndCommitInterlock=true,applicationSteeringAxes=true,evidence="Isolated IPC fixture"}},timeout.Token,Remember);
            await Serve(name,new{ok=true,result=new{generation=1,selected=false,pending=true,targetAddress="0"}},timeout.Token,Remember);
            await Serve(name,new{ok=true,result=new{generation=1,selected=true,pending=false,targetAddress="9007199254740995"}},timeout.Token,Remember);
            await Serve(name,new{ok=true,result=new{frame=new FlightFrame{Sequence=1,ObservedAt=DateTimeOffset.UtcNow,GameSha256=hash,Forward=new(0,0,1),Up=new(0,1,0)}}},timeout.Token,Remember);
            await Serve(name,new{ok=true,result=new{accepted=true}},timeout.Token,Remember);
            await Serve(name,new{ok=true,result=new{released=true}},timeout.Token,Remember);
        });
        var driver=new NativeFlightControlDriver(id,hash);await driver.PreflightAsync(timeout.Token);
        var next=new NavWaypoint(target,"Next",new(1,0,0),"G");var jump=new FlightDecision("Jump",new(1,0,0),1,false,false,true,60,"Ready");
        await Rejects<InvalidOperationException>(()=>driver.ApplyAsync(jump,1,next,DateTimeOffset.UtcNow.AddMilliseconds(250),timeout.Token));
        await driver.SelectTargetAsync(next,timeout.Token);
        await driver.ReadAsync(timeout.Token);
        await Rejects<InvalidOperationException>(()=>driver.ApplyAsync(jump with{Throttle=.75},1,next,DateTimeOffset.UtcNow.AddMilliseconds(250),timeout.Token));
        await driver.ApplyAsync(jump,1,next,DateTimeOffset.UtcNow.AddMilliseconds(250),timeout.Token);
        await driver.ReleaseAsync();await server;
        Check.Equal("target.select",requests[1].GetProperty("op").GetString()!);Check.Equal(target,requests[1].GetProperty("targetSystemAddress").GetInt64());
        Check.Equal("target.status",requests[2].GetProperty("op").GetString()!);Check.Equal("read",requests[3].GetProperty("op").GetString()!);Check.Equal("apply",requests[4].GetProperty("op").GetString()!);
        Check.Equal(1d,requests[4].GetProperty("decision").GetProperty("throttle").GetDouble());
    }
    private static PlayerState TargetArrivalPlayer(DateTimeOffset now)=>new(){InGame=true,CommanderId="F1",CurrentShipId=8,GameMode="Solo",LoadGameAt=now.AddHours(-1),
        Navigation=new(){CurrentSystemAddress=1,TargetSystemAddress=2,CurrentSystemAt=now.AddSeconds(-1),LastJumpAt=now.AddSeconds(-1),Route=[new(1,"Here",new(0,0,0),"G"),new(2,"Next",new(1,0,0),"G")]}};
    private static object TargetArrivalRaw(int state,bool manual=false,bool emergency=false)
    {
        var at=DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
        return new{sequence=7,observedAtUnixMilliseconds=at,entityId="42",referenceWorldAddress="1",localPlayerShipVerified=true,inGame=true,playerOverride=manual,rawFsdState=state,
            shipHealth=new{verified=true,sequence=7,observedAtUnixMilliseconds=at,entityId="42",hullFraction=1,emergency,damageObserved=false}};
    }
    [Test]public async Task JournalArrivalBeforeNativeJumpTailWaitsWithoutAnyFlightInput()
    {
        var id=Random.Shared.Next(100000000,190000000);var hash=new string('a',64);var name=$"XFEEDToolBox.Navigation.{id}";
        using var timeout=new CancellationTokenSource(TimeSpan.FromSeconds(6));var requests=new List<JsonElement>();
        void Remember(string text){using var doc=JsonDocument.Parse(text);requests.Add(doc.RootElement.Clone());}
        var server=Task.Run(async()=>
        {
            await Serve(name,new{ok=true,result=new{schema=1,name="XFEEDToolBox.Navigation",processId=id,gameSha256=hash,verified=true,
                continuousPose=true,continuousHeat=true,starEnvelopes=true,nativeSteering=true,chargeAndCommitInterlock=true,applicationSteeringAxes=true,evidence="Isolated arrival-tail fixture"}},timeout.Token,Remember);
            await Serve(name,new{ok=false,error="Select the destination before starting FSD charging"},timeout.Token,Remember);
            foreach(var (phase,owned) in new[]{(4,false),(3,true),(2,false)})
            {
                await Serve(name,new{ok=true,result=new{fresh=true,entityId="42",ownedCharging=owned,state=phase,targetValid=true,targetAddress="2"}},timeout.Token,Remember);
                await Serve(name,new{ok=true,result=TargetArrivalRaw(phase)},timeout.Token,Remember);
            }
            await Serve(name,new{ok=true,result=new{generation=7,selected=false,pending=true,targetAddress="2"}},timeout.Token,Remember);
            await Serve(name,new{ok=true,result=new{generation=7,selected=true,pending=false,targetAddress="2"}},timeout.Token,Remember);
            await Serve(name,new{ok=true,result=new{released=true}},timeout.Token,Remember);
        });
        var player=TargetArrivalPlayer(DateTimeOffset.UtcNow);var driver=new NativeFlightControlDriver(id,hash,()=>player);
        var observations=new List<JsonElement>();driver.TargetSelectionObserved+=observations.Add;
        await driver.PreflightAsync(timeout.Token);await driver.SelectTargetAsync(new(2,"Next",new(1,0,0),"G"),timeout.Token);await driver.ReleaseAsync();await server;
        Check.That(requests.All(r=>r.GetProperty("op").GetString() is "describe" or "target.select" or "target.status" or "fsd.describe" or "read" or "release"));
        Check.Equal(2,requests.Count(r=>r.GetProperty("op").GetString()=="target.select"));
        Check.Equal("acknowledged",observations[^1].GetProperty("phase").GetString()!);
        Check.That(observations.Any(o=>o.GetProperty("diagnostic").TryGetProperty("ownedCharging",out var owned)&&owned.ValueKind==JsonValueKind.True));
    }
    [Test]public async Task ArrivalSelectionManualOrHealthRejectionCannotIssueAnotherTargetOrFlightRequest()
    {
        foreach(var manual in new[]{true,false})
        {
            var id=Random.Shared.Next(100000000,190000000);var hash=new string('a',64);var name=$"XFEEDToolBox.Navigation.{id}";
            using var timeout=new CancellationTokenSource(TimeSpan.FromSeconds(5));var operations=new List<string>();
            void Remember(string text){using var doc=JsonDocument.Parse(text);operations.Add(doc.RootElement.GetProperty("op").GetString()!);}
            var server=Task.Run(async()=>
            {
                await Serve(name,new{ok=true,result=new{schema=1,name="XFEEDToolBox.Navigation",processId=id,gameSha256=hash,verified=true,continuousPose=true,continuousHeat=true,
                    starEnvelopes=true,nativeSteering=true,chargeAndCommitInterlock=true,evidence="Isolated arrival safety fixture"}},timeout.Token,Remember);
                await Serve(name,new{ok=false,error="Select the destination before starting FSD charging"},timeout.Token,Remember);
                await Serve(name,new{ok=true,result=new{fresh=true,entityId="42",ownedCharging=false,state=2,targetValid=true,targetAddress="2"}},timeout.Token,Remember);
                await Serve(name,new{ok=true,result=TargetArrivalRaw(2,manual,!manual)},timeout.Token,Remember);
                await Serve(name,new{ok=true,result=new{released=true}},timeout.Token,Remember);
            });
            var player=TargetArrivalPlayer(DateTimeOffset.UtcNow);var driver=new NativeFlightControlDriver(id,hash,()=>player);await driver.PreflightAsync(timeout.Token);
            await Rejects<InvalidOperationException>(()=>driver.SelectTargetAsync(new(2,"Next",new(1,0,0),"G"),timeout.Token));await driver.ReleaseAsync();await server;
            Check.Equal(1,operations.Count(o=>o=="target.select"));Check.That(!operations.Any(o=>o is "apply" or "calibrate.fsd" or "target.status"));
        }
    }
    [Test] public async Task ClearNormalSpaceDepartureDoesNotEnterSupercruiseOrWaitForSpeed()
    {
        var id=Random.Shared.Next(80000000,89000000);var hash=new string('a',64);var name=$"XFEEDToolBox.Navigation.{id}";
        using var timeout=new CancellationTokenSource(TimeSpan.FromSeconds(6));var requests=new List<string>();
        var frame=new FlightFrame{Sequence=1,GameSha256=hash,ObservedAt=DateTimeOffset.UtcNow,
            Position=new(1e6,0,0),TargetPosition=new(0,1e7,0),Forward=new(0,1,0),Up=new(0,0,1),CurrentStar=new(1,"G",Vector3d.Zero,100,null,null),
            InSupercruise=false,FuelMain=30,FuelNeededToNextRefuel=10,NativeAllowsCharging=true,SpeedMetresPerSecond=0,SpeedFraction=0,OutwardDepartureRequired=false};
        var server=Task.Run(async()=>
        {
            await Serve(name,new{ok=true,result=new{schema=1,name="XFEEDToolBox.Navigation",processId=id,gameSha256=hash,verified=true,
                continuousPose=true,continuousHeat=true,starEnvelopes=true,nativeSteering=true,chargeAndCommitInterlock=true,applicationSteeringAxes=true,evidence="Isolated reference fixture"}},timeout.Token,requests.Add);
            await Serve(name,new{ok=true,result=new{frame}},timeout.Token,requests.Add);
            await Serve(name,new{ok=true,result=new{ready=false,issue="original-flight-contexts-disabled-unconfirmed"}},timeout.Token,requests.Add);
            await Serve(name,new{ok=true,result=new{ready=true,magnitude=0}},timeout.Token,requests.Add);
        });
        var driver=new NativeFlightControlDriver(id,hash,()=>new(){InGame=true});await driver.PreflightAsync(timeout.Token);
        await driver.PrepareForNavigationAsync(timeout.Token);await server;
        Check.Equal(4,requests.Count);Check.That(requests.All(r=>!r.Contains("apply")&&!r.Contains("target.select")));
    }
    [Test] public async Task UnknownPhysicalSourceAndHeldRealInputRefuseLegacyPreparationBeforeAnyFlightCommand()
    {
        foreach(var missing in new[]{true,false})
        {
            var id=Random.Shared.Next(120000000,129000000);var hash=new string('a',64);var name=$"XFEEDToolBox.Navigation.{id}";
            using var timeout=new CancellationTokenSource(TimeSpan.FromSeconds(5));var requests=new List<string>();
            var frame=new FlightFrame{Sequence=1,GameSha256=hash,ObservedAt=DateTimeOffset.UtcNow,Position=new(1e6,0,0),
                TargetPosition=new(0,1e7,0),Forward=new(0,1,0),Up=new(0,0,1),CurrentStar=new(1,"G",Vector3d.Zero,100,null,null),
                FuelMain=30,FuelNeededToNextRefuel=10,NativeAllowsCharging=true};
            var server=Task.Run(async()=>
            {
                await Serve(name,new{ok=true,result=new{schema=1,name="XFEEDToolBox.Navigation",processId=id,gameSha256=hash,verified=true,
                    continuousPose=true,continuousHeat=true,starEnvelopes=true,nativeSteering=true,chargeAndCommitInterlock=true,evidence="Isolated physical-input fixture"}},timeout.Token,requests.Add);
                await Serve(name,new{ok=true,result=new{frame}},timeout.Token,requests.Add);
                await Serve(name,new{ok=true,result=new{ready=!missing,magnitude=missing?0:1,issue=missing?"missing-or-invalid-button:YawLeftButton":null}},timeout.Token,requests.Add);
            });
            var driver=new NativeFlightControlDriver(id,hash,()=>new(){InGame=true});await driver.PreflightAsync(timeout.Token);
            await Rejects<InvalidOperationException>(()=>driver.PrepareForNavigationAsync(timeout.Token));await server;
            Check.Equal(3,requests.Count);Check.That(requests.All(request=>Check.Json(request).GetProperty("op").GetString() is "describe" or "read" or "input.physical.read"));
        }
    }
    [Test] public async Task ANewPhysicalInputSceneWaitsForItsProofThenRequiresANewControlFrame()
    {
        var id=Random.Shared.Next(130000000,139000000);var hash=new string('a',64);var name=$"XFEEDToolBox.Navigation.{id}";
        using var timeout=new CancellationTokenSource(TimeSpan.FromSeconds(5));var requests=new List<string>();
        var server=Task.Run(async()=>
        {
            await Serve(name,new{ok=true,result=new{schema=1,name="XFEEDToolBox.Navigation",processId=id,gameSha256=hash,verified=true,
                continuousPose=true,continuousHeat=true,starEnvelopes=true,nativeSteering=true,chargeAndCommitInterlock=true,evidence="Isolated scene fixture"}},timeout.Token,requests.Add);
            await Serve(name,new{ok=false,error="Original physical input sources unavailable: original-flight-contexts-disabled-unconfirmed"},timeout.Token,requests.Add);
            await Serve(name,new{ok=true,result=new{released=true}},timeout.Token,requests.Add);
            await Serve(name,new{ok=true,result=new{ready=false,issue="original-flight-contexts-disabled-unconfirmed"}},timeout.Token,requests.Add);
            await Serve(name,new{ok=true,result=new{ready=true,magnitude=0}},timeout.Token,requests.Add);
        });
        var driver=new NativeFlightControlDriver(id,hash);await driver.PreflightAsync(timeout.Token);
        await Rejects<FlightTelemetryUnavailableException>(()=>driver.ApplyAsync(new("Align",null,0,false,false,false,null,"Scene proof"),1,
            new(2,"Next",new(0,0,1),"G"),DateTimeOffset.UtcNow.AddMilliseconds(250),timeout.Token));
        await server;
        Check.Equal(1,requests.Count(request=>Check.Json(request).GetProperty("op").GetString()=="apply"));
        Check.Equal("release",Check.Json(requests[2]).GetProperty("op").GetString()!);
        foreach(var unknown in new[]{"Original physical input sources unavailable: missing-analog:YawAxisRaw",
            "Original physical input sources unavailable: unobserved-original-relative-mouse-source"})
            Check.That(!NavigationRecovery.CanRetry(new InvalidOperationException(unknown),default));
    }
    [Test] public async Task LoadedLegacyOutwardPolicyUsesExistingInputOnlyAfterSoftwareSafetyChecks()
    {
        foreach(var mode in new[]{0,1,2})
        {
            var safe=mode!=1;var transition=mode==2;
            var id=Random.Shared.Next(110000000,119000000);var hash=new string('a',64);var name=$"XFEEDToolBox.Navigation.{id}";
            using var timeout=new CancellationTokenSource(TimeSpan.FromSeconds(5));var requests=new List<JsonElement>();
            void Remember(string text){using var document=JsonDocument.Parse(text);requests.Add(document.RootElement.Clone());}
            var at=DateTimeOffset.UtcNow;
            var frame=new FlightFrame{Sequence=1,ObservedAt=at,GameSha256=hash,GeometryVerified=true,AdaptiveFlightControl=true,JumpCommitInterlockVerified=true,NativeAllowsCharging=true,
                NativeFullChargePeak=safe?80:101,Fsd=FsdPhase.Idle,SystemAddress=1,TargetSystemAddress=2,HeatPercent=40,FuelMain=20,FuelRequiredForJump=5,FuelNeededToNextRefuel=6,
                Position=new(150,0,0),Forward=new Vector3d(-1,2,0).Unit(),Up=new(0,0,1),TargetPosition=new(-10000,20000,0),CurrentStar=new(1,"K",Vector3d.Zero,100,null,null)};
            var server=Task.Run(async()=>
            {
                await Serve(name,new{ok=true,result=new{schema=1,name="XFEEDToolBox.Navigation",processId=id,gameSha256=hash,verified=true,continuousPose=true,continuousHeat=true,starEnvelopes=true,nativeSteering=true,chargeAndCommitInterlock=true,applicationSteeringAxes=true,evidence="Isolated reference fixture"}},timeout.Token,Remember);
                await Serve(name,new{ok=true,result=new{generation=1,selected=true,targetAddress="2"}},timeout.Token,Remember);
                await Serve(name,new{ok=true,result=new{frame}},timeout.Token,Remember);
                await Serve(name,new{ok=false,error="Native departure must be outward before FSD charging"},timeout.Token,Remember);
                if(safe)
                {
                    await Serve(name,new{ok=true,result=new{accepted=true,predictedPeak=80,throttleCommand=0}},timeout.Token,Remember);
                    if(transition)
                    {
                        await Serve(name,new{ok=false,error="Hyperspace calibration requires a matching locked system target and idle FSD"},timeout.Token,Remember);
                        await Serve(name,new{ok=true,result=new{fresh=true,ownedCharging=false,state=2,targetValid=true,targetAddress="2"}},timeout.Token,Remember);
                    }
                    else await Serve(name,new{ok=true,result=new{accepted=true,generation=1}},timeout.Token,Remember);
                }
                await Serve(name,new{ok=true,result=new{released=true}},timeout.Token,Remember);
            });
            var next=new NavWaypoint(2,"Next",new(1,2,0),"K");var driver=new NativeFlightControlDriver(id,hash);
            await driver.PreflightAsync(timeout.Token);await driver.SelectTargetAsync(next,timeout.Token);await driver.ReadAsync(timeout.Token);
            var decision=new FlightDecision("Align and charge",frame.Forward,1,true,false,false,80,"Independent fixture");
            if(transition)await Rejects<FlightTelemetryUnavailableException>(()=>driver.ApplyAsync(decision,1,next,at.AddMilliseconds(300),timeout.Token));
            else if(safe)await driver.ApplyAsync(decision,1,next,at.AddMilliseconds(300),timeout.Token);
            else await Rejects<InvalidOperationException>(()=>driver.ApplyAsync(decision,1,next,at.AddMilliseconds(300),timeout.Token));
            await driver.ReleaseAsync();await server;
            var primitive=requests.Where(r=>r.GetProperty("op").GetString()=="calibrate.fsd").ToArray();
            Check.Equal(safe?1:0,primitive.Length);
            if(safe)
            {
                Check.Equal("beginHyperspace",primitive[0].GetProperty("desired").GetString());
                Check.Equal(90,primitive[0].GetProperty("heatCeilingPercent").GetInt32());
                Check.That(!requests.Where(r=>r.GetProperty("op").GetString()=="apply").Last().GetProperty("decision").GetProperty("beginCharging").GetBoolean());
            }
        }
    }
    [Test] public async Task ObscuringPlanetPreparesSupercruiseAwayFromPlanetInsteadOfDistantStar()
    {
        var id=Random.Shared.Next(90000000,99000000);var hash=new string('a',64);var name=$"XFEEDToolBox.Navigation.{id}";
        using var timeout=new CancellationTokenSource(TimeSpan.FromSeconds(8));var requests=new List<JsonElement>();
        void Remember(string text){using var document=JsonDocument.Parse(text);requests.Add(document.RootElement.Clone());}
        var frame=new FlightFrame{Sequence=1,GameSha256=hash,ObservedAt=DateTimeOffset.UtcNow,LoadoutFingerprint="fit",SystemAddress=1,
            Position=new(100000,0,0),TargetPosition=new(1e8,0,0),Forward=new(-1,0,0),Up=new(0,0,1),CurrentStar=new(1,"G",Vector3d.Zero,100,null,null),
            Obstacles=[new(11,"Unknown",new(110000,0,0),5000,null,null)],
            InSupercruise=false,FuelMain=30,FuelNeededToNextRefuel=10,NativeAllowsCharging=true,SpeedFraction=1,HeatPercent=30,Fsd=FsdPhase.Idle,OutwardDepartureRequired=false};
        var thermal=new ShipThermalCalibration{GameSha256=hash,LoadoutFingerprint="fit",StarBodyId=1,Verified=true,ExpiresAt=DateTimeOffset.UtcNow.AddMinutes(2),
            MaximumHeat=150,ChargeDurationUpperSeconds=15,ChargingGainUpperPerSecond=.1,ChargedHoldGainUpperPerSecond=.1,DistanceBands=[new(99900,3e12,1,1)],Evidence="Isolated thermal fixture"};
        var next=new NavWaypoint(2,"Next",new(1,0,0),"G");
        var server=Task.Run(async()=>
        {
            await Serve(name,new{ok=true,result=new{schema=1,name="XFEEDToolBox.Navigation",processId=id,gameSha256=hash,verified=true,
                continuousPose=true,continuousHeat=true,starEnvelopes=true,nativeSteering=true,chargeAndCommitInterlock=true,applicationSteeringAxes=true,evidence="Isolated reference fixture"}},timeout.Token,Remember);
            await Serve(name,new{ok=true,result=new{generation=1,selected=true,targetAddress="2"}},timeout.Token,Remember);
            await Serve(name,new{ok=true,result=new{frame=frame with{ObservedAt=DateTimeOffset.UtcNow},thermal}},timeout.Token,Remember);
            await Serve(name,new{ok=true,result=new{ready=true,magnitude=0}},timeout.Token,Remember);
            await Serve(name,new{ok=true,result=new{accepted=true}},timeout.Token,Remember);
            await Serve(name,new{ok=true,result=new{frame=frame with{InSupercruise=true,Sequence=2,ObservedAt=DateTimeOffset.UtcNow},thermal}},timeout.Token,Remember);
            await Serve(name,new{ok=true,result=new{released=true}},timeout.Token,Remember);
        });
        var driver=new NativeFlightControlDriver(id,hash,()=>new(){InGame=true,Navigation=new(){CurrentSystemAddress=1}});
        await driver.PreflightAsync(timeout.Token);await driver.SelectTargetAsync(next,timeout.Token);await driver.PrepareForNavigationAsync(timeout.Token);await server;
        var decision=requests.Single(r=>r.GetProperty("op").GetString()=="apply").GetProperty("decision");
        Check.That(decision.GetProperty("enterSupercruise").GetBoolean());Check.That(decision.GetProperty("beginCharging").GetBoolean());
        Check.Equal(-1d,decision.GetProperty("direction").GetProperty("x").GetDouble());
    }
}