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

XFEEDToolBox

【WinUI】XFE精英危险工具箱

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

namespace XFEEDToolBox.Test;

[TestFixture]
public sealed class AutomaticTradeMenuIntegrationTests
{
    private sealed class Commerce:IOriginalCommerceChannel
    {
        public string Identity{get;}=Guid.NewGuid().ToString("N");
        public bool RejectDescribe;public List<string> Calls=[];
        public Task<JsonElement> CallAsync(object request,CancellationToken token)
        {
            token.ThrowIfCancellationRequested();var op=JsonSerializer.SerializeToElement(request).GetProperty("op").GetString()!;Calls.Add(op);
            if(op=="describe")
            {
                if(RejectDescribe)throw new NotSupportedException("fixture interface unavailable");
                return Task.FromResult(JsonSerializer.SerializeToElement(new{schema=1,name="XFEEDToolBox.Navigation",verified=true,gameSha256=NativeSystemLocationReader.GameSha256}));
            }
            if(op=="commerce.inventory")return Task.FromResult(JsonSerializer.SerializeToElement(new{verified=true,count=0,capacity=16,credits=10000}));
            throw new InvalidOperationException("Unexpected operation; this acceptance fixture must never trade: "+op);
        }
    }
    private sealed class Menus:ITradePauseRecoveryChannel
    {
        public int Renewals,Ends;
        public Task PrepareAsync(CancellationToken token)=>Task.CompletedTask;
        public Task<JsonElement> ReadAsync(CancellationToken token)=>Task.FromResult(JsonSerializer.SerializeToElement(new
        {
            state="InGame",nextState="InGame",tradePauseIdentity=new{verified=true,entityId="42",hullFraction=1d,observedAtUnixMilliseconds=DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}
        }));
        public Task RenewAsync(string owner,string run,ulong entity,CancellationToken token){Renewals++;return Task.CompletedTask;}
        public Task ResumeAsync(string owner,string run,string request,ulong sequence,CancellationToken token)=>throw new InvalidOperationException("An InGame observation never needs a menu action.");
        public Task EndAsync(string owner,string run){Ends++;return Task.CompletedTask;}
    }
    private sealed class Lease(Func<bool> allowed,TradePauseRecoverySession session):IDisposable
    {
        public bool Disposed;public bool Allowed=>allowed();
        public Task StepAsync()=>session.StepAsync(default);
        public void Dispose(){Check.That(!Allowed);Disposed=true;session.EndAsync().GetAwaiter().GetResult();}
    }
    private static PlayerState Player=>new(){Ship="Type9",InGame=true,CommanderId="engine-menu-fixture",CurrentShipId=7,LoadGameAt=DateTimeOffset.UtcNow.AddMinutes(-1),
        CargoReliable=true,CreditsReliable=true,Credits=10000,CargoCapacity=16,Rebuy=100};
    private static AutomaticTradeConfiguration Configuration(string directory)=>new(){WorkingDirectory=directory,
        Source=new(){MarketId=1,Name="Source",SystemName="System",MaxLandingPad=LandingPad.Large},Destination=new(){MarketId=2,Name="Destination",SystemName="System",MaxLandingPad=LandingPad.Large}};

    [Test]public async Task ActualEngineRevokesBeforeRelogAndCreatesANewSessionScopeAfterFastReentry()
    {
        using var temporary=new TemporaryDirectory();using var cancellation=new CancellationTokenSource(TimeSpan.FromSeconds(5));
        var current=Player;var channel=new Commerce();var menus=new Menus();var leases=new List<Lease>();var visits=0;var recoveries=0;var released=0;var preflightObserved=false;
        var protectionChecks=0;var recoveryProtectionObserved=false;
        var driver=new NativeTradeCommandDriver(channel,()=>current,Path.Combine(temporary.Path,"operations"),async(_,_,token)=>
        {
            if(++visits==1)
            {
                Check.Equal(1,leases.Count);await leases[0].StepAsync();Check.Equal(1,menus.Renewals);
                throw new DockingHandoffException("fixture same-mode recovery",2);
            }
            Check.Equal(2,leases.Count);Check.That(leases[0].Disposed&&!leases[0].Allowed&&leases[1].Allowed);
            await leases[1].StepAsync();Check.That(menus.Renewals==2,"The replacement session must bind the new LoadGameAt rather than retaining the old baseline.");
            cancellation.Cancel();token.ThrowIfCancellationRequested();
        },_=>Task.CompletedTask,()=>{released++;return Task.CompletedTask;})
        {
            RunningMenuRecoveryFactory=(allowed,_)=>
            {
                var lease=new Lease(allowed,new TradePauseRecoverySession(menus,()=>current,allowed));leases.Add(lease);return lease;
            }
        };
        Check.Equal(0,leases.Count);
        var engine=new AutomaticTradeEngine(Configuration(temporary.Path),driver,channel,()=>current,_=>
        {
            protectionChecks++;
            if(visits==1&&leases[0].Disposed&&released==1)recoveryProtectionObserved=true;
            return Task.CompletedTask;
        },temporary.Path,_=>
        {
            Check.That(leases.Single().Disposed&&!leases.Single().Allowed);Check.Equal(1,menus.Ends);
            Check.That(recoveryProtectionObserved&&protectionChecks>=2,"A failed handoff requires released inputs and a fresh healthy context before recovery menu actions.");
            current=current with{LoadGameAt=current.LoadGameAt!.Value.AddSeconds(1)};recoveries++;return Task.CompletedTask;
        },validateFlight:_=>{Check.Equal(0,leases.Count);return Task.CompletedTask;});
        engine.Progress+=progress=>{if(progress.Phase=="preflight"){preflightObserved=true;Check.Equal(0,leases.Count);}};
        await engine.RunAsync(cancellation.Token).WaitAsync(TimeSpan.FromSeconds(3));
        Check.That(preflightObserved);Check.Equal(1,recoveries);Check.Equal(2,visits);Check.Equal(2,released);
        Check.That(leases.All(lease=>lease.Disposed&&!lease.Allowed));Check.That(channel.Calls.All(op=>op is "describe" or "commerce.inventory"));
    }
    [Test]public async Task AnActualProtectiveRestartBreaksSafetyWaitingWithoutAnyFurtherInput()
    {
        using var temporary=new TemporaryDirectory();var current=Player;var channel=new Commerce();var pending=false;var visits=0;var released=0;var safetyPause=false;
        var driver=new NativeTradeCommandDriver(channel,()=>current,Path.Combine(temporary.Path,"operations"),(_,_,_)=>
        {visits++;pending=true;return Task.FromException(new FlightThreatPauseException());},_=>Task.CompletedTask,()=>{released++;return Task.CompletedTask;});
        var engine=new AutomaticTradeEngine(Configuration(temporary.Path),driver,channel,()=>current,_=>Task.CompletedTask,temporary.Path,sessionRestartPending:()=>pending);
        engine.Progress+=progress=>safetyPause|=progress.Phase=="safety-pause";
        var restartObserved=false;try{await engine.RunAsync(default).WaitAsync(TimeSpan.FromSeconds(2));}catch(FlightSessionRestartRequiredException){restartObserved=true;}
        Check.That(restartObserved&&safetyPause);Check.Equal(1,visits);Check.That(released>=2);
        Check.That(channel.Calls.All(op=>op is "describe" or "commerce.inventory"));
    }
    [Test]public async Task ActualSessionRecoveriesRemainBoundedAfterManyTransientReads()
    {
        using var temporary=new TemporaryDirectory();var current=Player;var channel=new Commerce();var visits=0;var recoveries=0;
        var driver=new NativeTradeCommandDriver(channel,()=>current,Path.Combine(temporary.Path,"operations"),(_,_,_)=>
        {
            if(++visits%5!=0)throw new FlightTelemetryUnavailableException("fixture fresh-frame retry");
            throw new DockingHandoffException("fixture handoff still unavailable",2);
        },_=>Task.CompletedTask);
        var engine=new AutomaticTradeEngine(Configuration(temporary.Path),driver,channel,()=>current,_=>Task.CompletedTask,temporary.Path,_=>
        {recoveries++;current=current with{LoadGameAt=current.LoadGameAt!.Value.AddSeconds(1)};return Task.CompletedTask;});
        var refused=false;try{await engine.RunAsync(default).WaitAsync(TimeSpan.FromSeconds(5));}catch(DockingHandoffException){refused=true;}
        Check.That(refused);Check.Equal(3,recoveries);Check.Equal(20,visits);
        Check.That(channel.Calls.All(op=>op is "describe" or "commerce.inventory"));
    }
    [Test]public async Task EngineInterfaceAndFlightPreparationFailuresNeverCreateARunningScope()
    {
        foreach(var rejectDescribe in new[]{true,false})
        {
            using var temporary=new TemporaryDirectory();var current=Player;var channel=new Commerce{RejectDescribe=rejectDescribe};var scopes=0;var released=0;
            var driver=new NativeTradeCommandDriver(channel,()=>current,Path.Combine(temporary.Path,"operations"),(_,_,_)=>throw new InvalidOperationException("Flight must not start"),_=>Task.CompletedTask,
                ()=>{released++;return Task.CompletedTask;}){RunningMenuRecoveryFactory=(_,_)=>{scopes++;throw new InvalidOperationException("Scope must not start");}};
            var engine=new AutomaticTradeEngine(Configuration(temporary.Path),driver,channel,()=>current,_=>Task.CompletedTask,temporary.Path,
                validateFlight:_=>throw new NotSupportedException("fixture flight interface unavailable"));
            var refused=false;try{await engine.RunAsync(default);}catch(NotSupportedException){refused=true;}
            Check.That(refused);Check.Equal(0,scopes);Check.Equal(1,released);Check.That(channel.Calls.All(op=>op is "describe" or "commerce.inventory"));
        }
    }
}