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

XFEEDToolBox

【WinUI】XFE精英危险工具箱

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

namespace XFEEDToolBox.Test;

[TestFixture]
public class GameRoutePlotOptionsTests
{
    private const long Destination=9007199254740995;
    private static readonly NavWaypoint Target=new(Destination,"Destination",new(30,0,0),"K");
    private static readonly NavWaypoint[] Route=[new(1,"Origin",new(0,0,0),"F"),Target];
    private static PlayerState Player(int capacity=256)=>new(){InGame=true,CargoCapacity=capacity,Navigation=new(){CurrentSystemAddress=1,Route=Route}};

    private class LegacyDriver:IGameRoutePlanningDriver
    {
        public bool SupportsGameRoutePlanning=>true;
        public int LegacyRequests,Reads;
        public Task<long> RequestGameRouteAsync(NavWaypoint target,CancellationToken token)
        {token.ThrowIfCancellationRequested();Check.Equal(Target,target);LegacyRequests++;return Task.FromResult(77L);}
        public Task<GameRouteStatus> ReadGameRouteStatusAsync(long generation,long address,CancellationToken token)
        {
            token.ThrowIfCancellationRequested();Check.Equal(77L,generation);Check.Equal(Destination,address);Reads++;
            return Task.FromResult(new GameRouteStatus(77,true,false,false,Destination,Destination,true,true,Route.Length,GameRouteFingerprint.Compute(Route)));
        }
    }
    private sealed class OptionsDriver:LegacyDriver,IGameRouteOptionsPlanningDriver
    {
        public bool SupportsGameRouteOptions {get;set;}=true;
        public bool SupportsRouteStarClassFilter {get;set;}
        public bool SupportsRouteMapFilters {get;set;}
        public int OptionsRequests;
        public GameRoutePlotOptions? Submitted;
        public Task<long> RequestGameRouteAsync(NavWaypoint target,GameRoutePlotOptions options,CancellationToken token)
        {token.ThrowIfCancellationRequested();Check.Equal(Target,target);OptionsRequests++;Submitted=options;return Task.FromResult(77L);}
    }
    private static async Task Rejects<T>(Func<Task> action) where T:Exception
    {try{await action();}catch(T){return;}throw new InvalidOperationException("Expected "+typeof(T).Name);}

    [Test] public async Task NullOrEmptyPreserveUsesTheLegacyPathEvenWithoutAnOptionsCapability()
    {
        foreach(var driver in new LegacyDriver[]{new(),new OptionsDriver{SupportsGameRouteOptions=false}})
        {
            var planner=new GameRoutePlanner(()=>Player());
            foreach(var options in new GameRoutePlotOptions?[]{null,new()})
            {
                var result=await planner.PlanAsync(Target,driver,options:options);
                Check.That(ReferenceEquals(Route,result.Route));Check.Equal(Target,result.Next);
            }
            Check.Equal(2,driver.LegacyRequests);Check.Equal(2,driver.Reads);
            if(driver is OptionsDriver optional)Check.Equal(0,optional.OptionsRequests);
        }
        Check.That(!new GameRoutePlotOptions().HasOverrides);
        Check.That(new GameRoutePlotOptions{UseJetConeBoost=false}.HasOverrides);
        Check.That(new GameRoutePlotOptions{VirtualCargoMass=0}.HasOverrides);
    }

    [Test] public async Task ExplicitOptionsReachTheOptionalDriverWithoutLosingFalseZeroOrPreservedFields()
    {
        var driver=new OptionsDriver();var planner=new GameRoutePlanner(()=>Player());
        foreach(var options in new[]{new GameRoutePlotOptions{Mode=GameRoutePlotMode.Economical},
            new(){UseJetConeBoost=false},new(){VirtualCargoMass=0},
            new(){Mode=GameRoutePlotMode.FsdBoost,FsdBoostLevel=0,UseJetConeBoost=true,VirtualCargoMass=42.5}})
        {
            var result=await planner.PlanAsync(Target,driver,options:options);
            Check.That(ReferenceEquals(options,driver.Submitted));Check.Equal(Destination,result.Next.SystemAddress);
        }
        Check.Equal(4,driver.OptionsRequests);Check.Equal(0,driver.LegacyRequests);
    }

    [Test] public async Task UnsupportedOrInvalidOverridesAreRejectedBeforeRequestingAnyRoute()
    {
        foreach(var driver in new LegacyDriver[]{new(),new OptionsDriver{SupportsGameRouteOptions=false}})
        {
            await Rejects<InvalidOperationException>(()=>new GameRoutePlanner(()=>Player()).PlanAsync(Target,driver,options:new(){UseJetConeBoost=false}));
            Check.Equal(0,driver.LegacyRequests);Check.Equal(0,driver.Reads);
            if(driver is OptionsDriver optional)Check.Equal(0,optional.OptionsRequests);
        }
        var capable=new OptionsDriver();
        await Rejects<ArgumentOutOfRangeException>(()=>new GameRoutePlanner(()=>Player(16)).PlanAsync(Target,capable,options:new(){VirtualCargoMass=16.01}));
        await Rejects<ArgumentOutOfRangeException>(()=>new GameRoutePlanner(()=>Player()).PlanAsync(Target,capable,options:new(){VirtualCargoMass=double.NaN}));
        Check.Equal(0,capable.LegacyRequests);Check.Equal(0,capable.OptionsRequests);Check.Equal(0,capable.Reads);
    }

    [Test] public void ValidationCoversEnumsBoostModeFiniteCargoAndConfirmedCapacityBoundaries()
    {
        foreach(var mode in Enum.GetValues<GameRoutePlotMode>())new GameRoutePlotOptions{Mode=mode}.Validate();
        foreach(var level in new[]{0,1,2,3})new GameRoutePlotOptions{Mode=GameRoutePlotMode.FsdBoost,FsdBoostLevel=level}.Validate();
        Check.Throws<ArgumentOutOfRangeException>(()=>new GameRoutePlotOptions{Mode=(GameRoutePlotMode)(-1)}.Validate());
        Check.Throws<ArgumentOutOfRangeException>(()=>new GameRoutePlotOptions{Mode=(GameRoutePlotMode)3}.Validate());
        foreach(var level in new[]{-1,4})Check.Throws<ArgumentOutOfRangeException>(()=>new GameRoutePlotOptions{Mode=GameRoutePlotMode.FsdBoost,FsdBoostLevel=level}.Validate());
        foreach(var mode in new GameRoutePlotMode?[]{null,GameRoutePlotMode.Economical,GameRoutePlotMode.Fastest})
            Check.Throws<ArgumentException>(()=>new GameRoutePlotOptions{Mode=mode,FsdBoostLevel=1}.Validate());
        foreach(var mass in new[]{double.NaN,double.PositiveInfinity,double.NegativeInfinity,-.01,2048.01})
            Check.Throws<ArgumentOutOfRangeException>(()=>new GameRoutePlotOptions{VirtualCargoMass=mass}.Validate());
        new GameRoutePlotOptions{VirtualCargoMass=2048}.Validate();
        new GameRoutePlotOptions{VirtualCargoMass=256}.Validate(256);
        new GameRoutePlotOptions{VirtualCargoMass=0}.Validate(0);
        new GameRoutePlotOptions{VirtualCargoMass=0}.Validate(-1);
        Check.Throws<ArgumentOutOfRangeException>(()=>new GameRoutePlotOptions{VirtualCargoMass=.01}.Validate(0));
        Check.Throws<ArgumentOutOfRangeException>(()=>new GameRoutePlotOptions{VirtualCargoMass=256.01}.Validate(256));
    }

    private static object Hello(int id,string hash,bool? options,bool? starFilter=null,bool? mapFilters=null)
    {
        var result=new Dictionary<string,object?>
        {
            ["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 owned named-pipe route-options fixture"
        };
        if(options is {} supported)result["gameRouteOptions"]=supported;
        if(starFilter is{} starSupported)result["routeStarClassFilter"]=starSupported;
        if(mapFilters is{} mapSupported)result["routeMapFilters"]=mapSupported;
        return new{ok=true,result};
    }
    private static async Task Serve(string name,object response,List<JsonElement> requests,CancellationToken token)
    {
        await using var pipe=new NamedPipeServerStream(name,PipeDirection.InOut,1,PipeTransmissionMode.Byte,PipeOptions.Asynchronous);
        await pipe.WaitForConnectionAsync(token);
        var bytes=new byte[4096];var count=0;
        while(true)
        {
            var read=await pipe.ReadAsync(bytes.AsMemory(count),token);if(read==0)throw new EndOfStreamException();count+=read;
            try{using var document=JsonDocument.Parse(bytes.AsMemory(0,count));requests.Add(document.RootElement.Clone());break;}
            catch(JsonException)when(count<bytes.Length){ }
        }
        await pipe.WriteAsync(JsonSerializer.SerializeToUtf8Bytes(response,ToolboxSettings.JsonOptions),token);
        await pipe.WriteAsync(new byte[]{(byte)'\n'},token);await pipe.FlushAsync(token);
    }

    [Test] public async Task NativePipeUsesNumericModesNullableFieldsAndTheSameExactDestinationAndOwner()
    {
        var id=Random.Shared.Next(91000000,91999999);var hash=new string('a',64);var name=$"XFEEDToolBox.Navigation.{id}";
        using var stop=new CancellationTokenSource(TimeSpan.FromSeconds(8));var requests=new List<JsonElement>();
        var options=new[]{new GameRoutePlotOptions{Mode=GameRoutePlotMode.Economical},
            new(){Mode=GameRoutePlotMode.Fastest,UseJetConeBoost=false,VirtualCargoMass=42.5},
            new(){Mode=GameRoutePlotMode.FsdBoost,UseJetConeBoost=true,FsdBoostLevel=3,VirtualCargoMass=0},
            new(){UseJetConeBoost=false}};
        var server=Task.Run(async()=>
        {
            await Serve(name,Hello(id,hash,true),requests,stop.Token);
            for(var i=0;i<options.Length+1;i++)await Serve(name,new{ok=true,result=new{generation=81L+i}},requests,stop.Token);
        });
        var driver=new NativeFlightControlDriver(id,hash,()=>Player());
        Check.That((await driver.PreflightAsync(stop.Token)).Ready&&driver.SupportsGameRouteOptions);
        await Rejects<ArgumentOutOfRangeException>(()=>driver.RequestGameRouteAsync(Target,new(){VirtualCargoMass=257},stop.Token));
        await Rejects<ArgumentOutOfRangeException>(()=>driver.RequestGameRouteAsync(Target,new(){Mode=(GameRoutePlotMode)3},stop.Token));
        await Rejects<ArgumentOutOfRangeException>(()=>driver.RequestGameRouteAsync(Target,new(){VirtualCargoMass=double.NaN},stop.Token));
        await Rejects<ArgumentException>(()=>driver.RequestGameRouteAsync(Target,new(){Mode=GameRoutePlotMode.Fastest,FsdBoostLevel=1},stop.Token));
        for(var i=0;i<options.Length;i++)Check.Equal(81L+i,await driver.RequestGameRouteAsync(Target,options[i],stop.Token));
        Check.Equal(85L,await driver.RequestGameRouteAsync(Target,stop.Token));await server;
        Check.Equal("describe",requests[0].GetProperty("op").GetString());
        for(var i=0;i<options.Length;i++)
        {
            var request=requests[i+1];Check.Equal("route.plan",request.GetProperty("op").GetString());
            Check.Equal(Destination,request.GetProperty("destinationSystemAddress").GetInt64());
            Check.Equal(requests[1].GetProperty("owner").GetString(),request.GetProperty("owner").GetString());
            var actual=request.GetProperty("routeOptions");var expected=options[i];
            Check.That(actual.GetProperty("mode").ValueKind==(expected.Mode is null?JsonValueKind.Null:JsonValueKind.Number));
            if(expected.Mode is {} mode)Check.Equal((int)mode,actual.GetProperty("mode").GetInt32());
            if(expected.UseJetConeBoost is {} boost)Check.Equal(boost,actual.GetProperty("useJetConeBoost").GetBoolean());
            else Check.Equal(JsonValueKind.Null,actual.GetProperty("useJetConeBoost").ValueKind);
            if(expected.FsdBoostLevel is {} level)Check.Equal(level,actual.GetProperty("fsdBoostLevel").GetInt32());
            else Check.Equal(JsonValueKind.Null,actual.GetProperty("fsdBoostLevel").ValueKind);
            if(expected.VirtualCargoMass is {} mass)Check.Equal(mass,actual.GetProperty("virtualCargoMass").GetDouble());
            else Check.Equal(JsonValueKind.Null,actual.GetProperty("virtualCargoMass").ValueKind);
        }
        Check.That(!requests[^1].TryGetProperty("routeOptions",out _));
        Check.Equal(requests[1].GetProperty("owner").GetString(),requests[^1].GetProperty("owner").GetString());
    }

    [Test] public async Task MissingNativeCapabilityRejectsOverridesLocallyAndStillAllowsTheLegacyRequest()
    {
        var id=Random.Shared.Next(92000000,92999999);var hash=new string('b',64);var name=$"XFEEDToolBox.Navigation.{id}";
        using var stop=new CancellationTokenSource(TimeSpan.FromSeconds(5));var requests=new List<JsonElement>();
        var server=Task.Run(async()=>
        {
            // The old describe response entirely omits this capability.
            await Serve(name,Hello(id,hash,null),requests,stop.Token);
            await Serve(name,new{ok=true,result=new{generation=91L}},requests,stop.Token);
        });
        var driver=new NativeFlightControlDriver(id,hash,()=>Player(8));Check.That((await driver.PreflightAsync(stop.Token)).Ready);
        Check.That(!driver.SupportsGameRouteOptions);
        await Rejects<InvalidOperationException>(()=>driver.RequestGameRouteAsync(Target,new(){UseJetConeBoost=false},stop.Token));
        Check.Equal(91L,await driver.RequestGameRouteAsync(Target,stop.Token));await server;
        Check.Equal(2,requests.Count);Check.That(!requests[1].TryGetProperty("routeOptions",out _));
    }

    [Test] public async Task StarClassRouteFilterRequiresItsOwnCapabilityBeforeAnyPlannerRequest()
    {
        var driver=new OptionsDriver();var options=new GameRoutePlotOptions{StarClassFilter=GameRouteStarClassFilter.Scoopable};
        Check.That(options.HasOverrides);
        await Rejects<InvalidOperationException>(()=>new GameRoutePlanner(()=>Player()).PlanAsync(Target,driver,options:options));
        Check.Equal(0,driver.OptionsRequests);Check.Equal(0,driver.Reads);
        driver.SupportsRouteStarClassFilter=true;
        await new GameRoutePlanner(()=>Player()).PlanAsync(Target,driver,options:options);
        Check.Equal(0x807Fu,driver.Submitted!.StarClassFilter!.OriginalMask);Check.Equal(1,driver.OptionsRequests);
    }

    [Test] public async Task NativeStarClassMaskPreservesExplicitEmptyAndDisallowsOldOptionsOnlyPlugins()
    {
        foreach(var supported in new[]{false,true})
        {
            var id=Random.Shared.Next(93000000,93999999);var hash=new string('c',64);var name=$"XFEEDToolBox.Navigation.{id}";
            using var stop=new CancellationTokenSource(TimeSpan.FromSeconds(6));var requests=new List<JsonElement>();
            var server=Task.Run(async()=>
            {
                await Serve(name,Hello(id,hash,true,supported?true:null),requests,stop.Token);
                if(supported)for(var i=0;i<2;i++)await Serve(name,new{ok=true,result=new{generation=101L+i}},requests,stop.Token);
            });
            var driver=new NativeFlightControlDriver(id,hash,()=>Player());Check.That((await driver.PreflightAsync(stop.Token)).Ready);
            Check.Equal(supported,driver.SupportsRouteStarClassFilter);
            var empty=new GameRoutePlotOptions{StarClassFilter=GameRouteStarClassFilter.FromOriginalMask(0)};
            if(!supported)await Rejects<InvalidOperationException>(()=>driver.RequestGameRouteAsync(Target,empty,stop.Token));
            else
            {
                await driver.RequestGameRouteAsync(Target,empty,stop.Token);
                await driver.RequestGameRouteAsync(Target,new(){StarClassFilter=GameRouteStarClassFilter.Scoopable},stop.Token);
            }
            await server;Check.Equal(supported?3:1,requests.Count);
            if(supported)
            {
                Check.Equal(0u,requests[1].GetProperty("routeOptions").GetProperty("starClassMask").GetUInt32());
                Check.Equal(0x807Fu,requests[2].GetProperty("routeOptions").GetProperty("starClassMask").GetUInt32());
                Check.Equal(Destination,requests[2].GetProperty("destinationSystemAddress").GetInt64());
            }
        }
    }

    [Test] public async Task GenericRouteMapFilterRequiresAnIndependentPlannerCapability()
    {
        var driver=new OptionsDriver{SupportsRouteStarClassFilter=true};
        var options=new GameRoutePlotOptions{UseJetConeBoost=false,VirtualCargoMass=0,
            MapFilter=new(){CategoryId=7,ExpectedKeys=["anarchy","lawless","lowsecurity"],SelectedKeys=[],AffectsRoutePlotting=false}};
        await Rejects<InvalidOperationException>(()=>new GameRoutePlanner(()=>Player()).PlanAsync(Target,driver,options:options));
        Check.Equal(0,driver.OptionsRequests);Check.Equal(0,driver.LegacyRequests);Check.Equal(0,driver.Reads);
        driver.SupportsRouteMapFilters=true;
        await new GameRoutePlanner(()=>Player()).PlanAsync(Target,driver,options:options);
        Check.That(ReferenceEquals(options,driver.Submitted)&&ReferenceEquals(options.MapFilter,driver.Submitted!.MapFilter));
        Check.That(driver.Submitted!.MapFilter is{SelectedKeys.Count:0,AffectsRoutePlotting:false});Check.Equal(1,driver.OptionsRequests);
    }

    [Test] public async Task MissingOrFalseMapCapabilityRejectsLocallyWithoutDisablingStarOrLegacyRoutes()
    {
        foreach(var capability in new bool?[]{null,false})
        {
            var id=Random.Shared.Next(94000000,94999999);var hash=new string('d',64);var name=$"XFEEDToolBox.Navigation.{id}";
            using var stop=new CancellationTokenSource(TimeSpan.FromSeconds(6));var requests=new List<JsonElement>();
            var server=Task.Run(async()=>
            {
                await Serve(name,Hello(id,hash,true,true,capability),requests,stop.Token);
                await Serve(name,new{ok=true,result=new{generation=111L}},requests,stop.Token);
                await Serve(name,new{ok=true,result=new{generation=112L}},requests,stop.Token);
            });
            var driver=new NativeFlightControlDriver(id,hash,()=>Player());Check.That((await driver.PreflightAsync(stop.Token)).Ready);
            Check.That(driver.SupportsGameRouteOptions&&driver.SupportsRouteStarClassFilter&&!driver.SupportsRouteMapFilters);
            await Rejects<InvalidOperationException>(()=>driver.RequestGameRouteAsync(Target,new(){UseJetConeBoost=false,VirtualCargoMass=0,
                MapFilter=new(){CategoryId=8,ExpectedKeys=["notvisited","visited"],SelectedKeys=[],AffectsRoutePlotting=false}},stop.Token));
            Check.Equal(111L,await driver.RequestGameRouteAsync(Target,new(){StarClassFilter=GameRouteStarClassFilter.Scoopable},stop.Token));
            Check.Equal(112L,await driver.RequestGameRouteAsync(Target,stop.Token));await server;
            Check.Equal(3,requests.Count);Check.Equal(0x807Fu,requests[1].GetProperty("routeOptions").GetProperty("starClassMask").GetUInt32());
            Check.Equal(JsonValueKind.Null,requests[1].GetProperty("routeOptions").GetProperty("mapFilter").ValueKind);
            Check.That(!requests[2].TryGetProperty("routeOptions",out _));
        }
    }

    [Test] public async Task NativeMapWirePreservesExactNamesExplicitEmptyFalseAndEveryOtherOverride()
    {
        var id=Random.Shared.Next(95000000,95999999);var hash=new string('e',64);var name=$"XFEEDToolBox.Navigation.{id}";
        using var stop=new CancellationTokenSource(TimeSpan.FromSeconds(6));var requests=new List<JsonElement>();
        var options=new[]{new GameRoutePlotOptions{Mode=GameRoutePlotMode.Fastest,UseJetConeBoost=false,VirtualCargoMass=12.5,
                MapFilter=new(){CategoryId=7,ExpectedKeys=["lowsecurity","lawless","anarchy"],SelectedKeys=["lawless"],AffectsRoutePlotting=false}},
            new(){Mode=GameRoutePlotMode.FsdBoost,FsdBoostLevel=0,UseJetConeBoost=false,VirtualCargoMass=0,StarClassFilter=GameRouteStarClassFilter.Scoopable,
                MapFilter=new(){CategoryId=3,ExpectedKeys=["democracy","anarchy","corporate"],SelectedKeys=[],AffectsRoutePlotting=false}}};
        var server=Task.Run(async()=>
        {
            await Serve(name,Hello(id,hash,true,true,true),requests,stop.Token);
            for(var i=0;i<options.Length;i++)await Serve(name,new{ok=true,result=new{generation=121L+i}},requests,stop.Token);
        });
        var driver=new NativeFlightControlDriver(id,hash,()=>Player());Check.That((await driver.PreflightAsync(stop.Token)).Ready&&driver.SupportsRouteMapFilters);
        for(var i=0;i<options.Length;i++)Check.Equal(121L+i,await driver.RequestGameRouteAsync(Target,options[i],stop.Token));await server;
        Check.Equal(3,requests.Count);
        for(var i=0;i<options.Length;i++)
        {
            var request=requests[i+1];var actual=request.GetProperty("routeOptions");var map=actual.GetProperty("mapFilter");var expected=options[i];
            Check.Equal("route.plan",request.GetProperty("op").GetString());Check.Equal(Destination,request.GetProperty("destinationSystemAddress").GetInt64());
            Check.Equal(requests[1].GetProperty("owner").GetString(),request.GetProperty("owner").GetString());
            Check.Equal(expected.MapFilter!.CategoryId,map.GetProperty("categoryId").GetInt32());
            Check.That(map.GetProperty("expectedKeys").EnumerateArray().Select(x=>x.GetString()).SequenceEqual(expected.MapFilter.ExpectedKeys));
            Check.That(map.GetProperty("selectedKeys").EnumerateArray().Select(x=>x.GetString()).SequenceEqual(expected.MapFilter.SelectedKeys));
            Check.That(!map.GetProperty("affectsRoutePlotting").GetBoolean());
            Check.Equal((int)expected.Mode!.Value,actual.GetProperty("mode").GetInt32());Check.That(!actual.GetProperty("useJetConeBoost").GetBoolean());
            Check.Equal(expected.VirtualCargoMass!.Value,actual.GetProperty("virtualCargoMass").GetDouble());
            if(expected.FsdBoostLevel is{} level)Check.Equal(level,actual.GetProperty("fsdBoostLevel").GetInt32());
            else Check.Equal(JsonValueKind.Null,actual.GetProperty("fsdBoostLevel").ValueKind);
            if(expected.StarClassFilter is{} stars)Check.Equal(stars.OriginalMask,actual.GetProperty("starClassMask").GetUInt32());
            else Check.Equal(JsonValueKind.Null,actual.GetProperty("starClassMask").ValueKind);
        }
        Check.Equal("lawless",requests[1].GetProperty("routeOptions").GetProperty("mapFilter").GetProperty("selectedKeys")[0].GetString());
        Check.Equal(0,requests[2].GetProperty("routeOptions").GetProperty("mapFilter").GetProperty("selectedKeys").GetArrayLength());
    }

    [Test] public async Task GenericMapCapabilityDoesNotRequireOrImplyTheStarClassCapability()
    {
        var id=Random.Shared.Next(96000000,96999999);var hash=new string('f',64);var name=$"XFEEDToolBox.Navigation.{id}";
        using var stop=new CancellationTokenSource(TimeSpan.FromSeconds(6));var requests=new List<JsonElement>();
        var server=Task.Run(async()=>
        {
            await Serve(name,Hello(id,hash,true,null,true),requests,stop.Token);
            await Serve(name,new{ok=true,result=new{generation=131L}},requests,stop.Token);
        });
        var driver=new NativeFlightControlDriver(id,hash,()=>Player());Check.That((await driver.PreflightAsync(stop.Token)).Ready);
        Check.That(driver.SupportsRouteMapFilters&&!driver.SupportsRouteStarClassFilter);
        await Rejects<InvalidOperationException>(()=>driver.RequestGameRouteAsync(Target,new(){StarClassFilter=GameRouteStarClassFilter.Scoopable},stop.Token));
        Check.Equal(131L,await driver.RequestGameRouteAsync(Target,new(){MapFilter=new(){CategoryId=8,ExpectedKeys=["visited","notvisited"],SelectedKeys=[],AffectsRoutePlotting=false}},stop.Token));
        await server;Check.Equal(2,requests.Count);Check.Equal(JsonValueKind.Null,requests[1].GetProperty("routeOptions").GetProperty("starClassMask").ValueKind);
        Check.Equal(0,requests[1].GetProperty("routeOptions").GetProperty("mapFilter").GetProperty("selectedKeys").GetArrayLength());
    }
}