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

XFEEDToolBox

【WinUI】XFE精英危险工具箱

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

namespace XFEEDToolBox.Test;

[TestFixture]
public class GalaxyQueryServiceTests
{
    [Test] public async Task EconomyQueriesBothActualFieldsWithinTheDisplayedVolumeAndReturnsMeasuredStars()
    {
        var handler=new Api();using var http=new HttpClient(handler);var query=new GalaxyQueryService(http);
        var filter=new GalaxyMapFilter{Mode=GalaxyMapMode.Map,Category=GalaxyFilterCategory.Economy,Values=new HashSet<string>{"hightech"}};
        var result=await query.QueryAsync(filter,new(0,0,0),30);
        Check.Equal(2,handler.Requests.Count(x=>x.Url.EndsWith("/systems/search")));
        var first=handler.Requests.First(x=>x.Url.EndsWith("/systems/search")).Json;
        Check.Equal(30d,first.GetProperty("filters").GetProperty("distance").GetProperty("max").GetDouble());
        Check.Equal("High Tech",first.GetProperty("filters").GetProperty("primary_economy").GetProperty("value")[0].GetString());
        Check.That(handler.Requests.Any(x=>x.Json.ValueKind==JsonValueKind.Object&&x.Json.TryGetProperty("filters",out var f)&&f.TryGetProperty("secondary_economy",out _)));
        Check.Equal(1,result.Systems.Count);Check.Equal("Measured",result.Systems[0].Name);Check.Equal(new Coordinates(2,3,4),result.Systems[0].Position);Check.That(result.Systems[0].Data.Visited is null);
        Check.That(filter.Matches(result.Systems[0]));Check.That(!filter.Matches(new("Unknown",new(0,0,0))));
    }
    [Test] public async Task ServiceAndCommodityQueriesUseVerifiedNestedStationSchemaAndRealSubtypeFacts()
    {
        var handler=new Api();using var http=new HttpClient(handler);var query=new GalaxyQueryService(http);
        var service=new GalaxyMapFilter{Mode=GalaxyMapMode.Map,Category=GalaxyFilterCategory.Services,Values=new HashSet<string>{"materialtraderraw"}};
        var result=await query.QueryAsync(service,new(0,0,0),50);
        var request=handler.Requests.Last(x=>x.Url.EndsWith("/stations/search")).Json;
        Check.Equal("Raw",request.GetProperty("filters").GetProperty("material_trader").GetProperty("value")[0].GetString());
        Check.That(result.Systems.Single().Data.Services!.Contains("materialtraderraw"));
        var commodity=service with{Category=GalaxyFilterCategory.Commodity,Values=new HashSet<string>{"gold"}};
        result=await query.QueryAsync(commodity,new(0,0,0),50);
        var market=handler.Requests.Last(x=>x.Url.EndsWith("/stations/search")).Json.GetProperty("filters").GetProperty("marketplace")[0];
        Check.Equal("Gold",market.GetProperty("commodity").GetProperty("value")[0].GetString());Check.Equal(1L,market.GetProperty("supply").GetProperty("value")[0].GetInt64());
        Check.That(result.Systems.Single().Data.Commodities!.Contains("Gold"));
        var general=service with{Values=new HashSet<string>{"rearm"}};await query.QueryAsync(general,new(0,0,0),50);
        var nested=handler.Requests.Last(x=>x.Url.EndsWith("/systems/search")).Json.GetProperty("filters").GetProperty("stations")[0];
        Check.Equal("Restock",nested.GetProperty("services").GetProperty("value")[0].GetString());
    }
    [Test] public void RichCatalogueDataKeepsRealControlSeparateFromInfluenceAndDoesNotInventPlayerState()
    {
        var filter=new GalaxyMapFilter{Mode=GalaxyMapMode.Powerplay,Category=GalaxyFilterCategory.Power,Values=new HashSet<string>{"actualcontroller"}};
        var root=Check.Json("""{"results":[{"name":"Measured","id64":27,"x":1,"y":2,"z":3,"power":["Other influence"],"controlling_power":"Actual controller","power_state":"Stronghold","population":1000,"is_colonised":true,"power_state_reinforcement":15,"stations":[{"type":"Drake-Class Carrier","services":["Refuel","Restock"]}],"bodies":[{"is_main_star":true,"subtype":"G (White-Yellow) Star"},{"landmarks":[{"type":"Guardian Ruin"}]}]}]}""");
        var s=GalaxyQueryService.Parse(root,"systems",filter,true,new(0,0,0),20).Single();
        Check.Equal("Actual controller",s.Data.Power);Check.Equal("Stronghold",s.Data.PowerState);Check.Equal("colonised",s.Data.Colonisation);Check.That(s.Data.Civilizations!.Contains("guardian"));Check.That(s.Data.PowerActivities!.Contains("reinforcement"));
        Check.That(s.Data.FleetCarrier==true&&s.Data.Visited is null&&s.Data.Exploration is null&&s.Data.TradeData is null);
        Check.That(GalaxyQueryService.Parse(root,"systems",filter,true,new(1000,0,0),1).Count==0);
    }
    [Test] public async Task CommodityImportDemandIsIndependentOfExportSupplyAndOldStationMarketsCannotRestoreClearedSupply()
    {
        using var http=new HttpClient(new Api());var service=new GalaxyQueryService(http);
        var exports=new GalaxyMapFilter{Mode=GalaxyMapMode.Map,Category=GalaxyFilterCategory.Commodity,Values=new HashSet<string>{"gold"}};
        var imports=exports with{CommodityDirection=GalaxyCommodityDirection.Imports};var plan=(await service.PlansAsync(imports)).Single();
        var payload=JsonSerializer.SerializeToElement(plan.Filters).GetProperty("marketplace")[0];
        Check.Equal(1L,payload.GetProperty("demand").GetProperty("value")[0].GetInt64());Check.That(!payload.TryGetProperty("supply",out _));Check.That(exports.QueryKey!=imports.QueryKey);
        GalacticSystem Read(string time,int supply,int demand,GalaxyMapFilter filter)=>GalaxyQueryService.Parse(Check.Json($$"""{"results":[{"name":"Ehrlich City","system_name":"Sol","system_id64":1,"system_x":0,"system_y":0,"system_z":0,"market_updated_at":"{{time}}","market":[{"commodity":"Gold","supply":{{supply}},"demand":{{demand}}}]}]}"""),"stations",filter,true,new(0,0,0),50).Single();
        var previous=Read("2026-10-22T00:00:00Z",3,0,exports);var current=Read("2026-10-23T00:00:00Z",0,38515,imports);
        Check.That(imports.Matches(current)&&!exports.Matches(current));
        var merged=current with{Data=previous.Data.Merge(current.Data)};Check.That(imports.Matches(merged)&&!exports.Matches(merged));
        var late=merged with{Data=merged.Data.Merge(previous.Data)};Check.That(imports.Matches(late)&&!exports.Matches(late));
        Check.That(ReferenceEquals(merged.Data,merged.Data.Merge(current.Data)));
        var other=new GalaxySystemData{StationMarkets=new Dictionary<string,GalaxyStationMarket>{{"Independent station",new(["Gold"],[],DateTimeOffset.Parse("2026-10-23T00:00:00Z"))}}};
        Check.That(exports.Matches(merged with{Data=merged.Data.Merge(other)}));
    }
    [Test] public async Task UnsupportedPlayerOwnershipIsNeverTranslatedIntoAPublicPopulationGuess()
    {
        var handler=new Api();using var http=new HttpClient(handler);var query=new GalaxyQueryService(http);
        foreach(var category in new[]{GalaxyFilterCategory.Visited,GalaxyFilterCategory.Exploration,GalaxyFilterCategory.TradeData})
        {Check.That(!GalaxyQueryService.UsesPublicCatalog(category));Check.Equal(0,(await query.PlansAsync(new(){Mode=GalaxyMapMode.Map,Category=category})).Count);}
        var owned=new GalaxyMapFilter{Mode=GalaxyMapMode.Colonisation,Category=GalaxyFilterCategory.Colonisation,Values=new HashSet<string>{"localplayerclaimed"}};
        Check.Equal(0,(await query.PlansAsync(owned)).Count);Check.Equal(0,handler.Requests.Count);
    }
    [Test] public async Task CancelledSupersededSearchCannotPublishAnIgnoringHandlersLateResponse()
    {
        var handler=new Api{SlowFirstSearch=true};using var http=new HttpClient(handler);var query=new GalaxyQueryService(http);using var stop=new CancellationTokenSource();
        var filter=new GalaxyMapFilter{Mode=GalaxyMapMode.Map,Category=GalaxyFilterCategory.Power,Values=new HashSet<string>{"actualcontroller"}};
        var published=0;var stale=query.QueryAsync(filter,new(0,0,0),50,stop.Token,_=>published++);
        await handler.SearchStarted.Task;stop.Cancel();var fresh=await query.QueryAsync(filter,new(0,0,0),50);
        try{await stale;throw new InvalidOperationException("Cancelled search unexpectedly completed.");}catch(OperationCanceledException){ }
        Check.Equal(0,published);Check.Equal(1,fresh.Systems.Count);
    }
    [Test] public async Task PerspectiveQueryIncludesActualVisibleStarsFarBeyondAPivotRadiusAndPublishesItsExactBounds()
    {
        var camera=GalaxyCamera.Create(new(0,0,0),1600,900,0,.4,.1);
        var d=camera.Forward*(camera.Distance*3)+camera.Right*camera.Distance;
        var point=new Coordinates(camera.Eye.X+d.X,camera.Eye.Y+d.Y,camera.Eye.Z+d.Z);
        Check.That(camera.Project(point).IsVisible&&point.DistanceTo(camera.Focus)>1000);
        var viewport=GalaxyViewport.Create(new(0,0,0),1600,900,0,.4,.1,0,0) with{Camera=camera};
        var handler=new Api{Point=point};using var http=new HttpClient(handler);var query=new GalaxyQueryService(http);
        var filter=new GalaxyMapFilter{Mode=GalaxyMapMode.Map,Category=GalaxyFilterCategory.Power,Values=new HashSet<string>{"actualcontroller"}};
        var progress=new List<GalaxyQueryResult>();var result=await query.QueryViewportAsync(filter,viewport,progress:progress.Add);
        Check.Equal(1,result.Systems.Count);Check.That(result.Bounds!.Contains(point));Check.That(result.CoverageComplete);Check.That(progress.Count>0);
        var request=handler.Requests.First(x=>x.Url.EndsWith("/systems/search")).Json;
        Check.That(!request.GetProperty("filters").TryGetProperty("distance",out _));
        var x=request.GetProperty("filters").GetProperty("x").GetProperty("value");Check.That(x[0].GetDouble()<=point.X&&x[1].GetDouble()>=point.X);
    }
    [Test] public async Task DensePublicPartitionsRefineInsteadOfStoppingAtAnArbitraryNearestRowsLimit()
    {
        var handler=new Api{DenseFirstSearch=true};using var http=new HttpClient(handler);var query=new GalaxyQueryService(http);
        var viewport=new GalaxyViewport(new(0,0,0),new(1,0,0),new(0,1,0),new(0,0,1),2000,2000,2000);
        var filter=new GalaxyMapFilter{Mode=GalaxyMapMode.Map,Category=GalaxyFilterCategory.Power,Values=new HashSet<string>{"actualcontroller"}};
        var result=await query.QueryViewportAsync(filter,viewport);
        Check.That(result.CompletedQueries>=9&&result.CoverageComplete);Check.Equal(0,result.PendingQueries);Check.That(!result.Truncated);
        var searches=handler.Requests.Where(x=>x.Url.EndsWith("/systems/search")).ToArray();Check.That(searches.Length>=9);
        Check.That(searches.Skip(1).Any(x=>x.Json.GetProperty("filters").GetProperty("x").GetProperty("value")[1].GetDouble()-x.Json.GetProperty("filters").GetProperty("x").GetProperty("value")[0].GetDouble()<4000));
    }
    [Test] public async Task StarportMarkerUsesActualStationStateInsteadOfSubstitutingBgsWar()
    {
        var handler=new Api();using var http=new HttpClient(handler);var query=new GalaxyQueryService(http);
        var filter=new GalaxyMapFilter{Mode=GalaxyMapMode.Map,Category=GalaxyFilterCategory.Markers,Values=new HashSet<string>{"repairing"}};
        var plans=await query.PlansAsync(filter);Check.Equal(1,plans.Count);Check.Equal("stations",plans[0].Index);
        var payload=JsonSerializer.SerializeToElement(plans[0].Filters);Check.Equal("UnderRepairs",payload.GetProperty("state").GetProperty("value")[0].GetString());
        var rows=Check.Json("""{"results":[{"system_name":"Repair","system_x":0,"system_y":0,"system_z":0,"state":"UnderRepairs","controlling_minor_faction_state":"War"},{"system_name":"JustWar","system_x":1,"system_y":0,"system_z":0,"controlling_minor_faction_state":"War"}]}""");
        var parsed=GalaxyQueryService.Parse(rows,"stations",filter,false,new(0,0,0),10);
        Check.That(filter.Matches(parsed[0]));Check.That(!filter.Matches(parsed[1]));
    }
    [Test] public async Task PrivateColonialEligibilityAndUnresolvedMarkersNeverClaimCompletePublicCoverage()
    {
        var handler=new Api();using var http=new HttpClient(handler);var query=new GalaxyQueryService(http);var viewport=new GalaxyViewport(new(0,0,0),new(1,0,0),new(0,1,0),new(0,0,1),10,10,10);
        foreach(var filter in new[]{new GalaxyMapFilter{Mode=GalaxyMapMode.Colonisation,Category=GalaxyFilterCategory.Colonisation,Values=new HashSet<string>{"localplayerclaimed"}},new(){Mode=GalaxyMapMode.Map,Category=GalaxyFilterCategory.Markers,Values=new HashSet<string>{"friend"}}})
        {var result=await query.QueryViewportAsync(filter,viewport);Check.That(!result.CoverageComplete);Check.Equal(0,result.Systems.Count);}
        Check.Equal(0,handler.Requests.Count);
    }
    [Test] public async Task MissingOrPartlyUnsupportedPublicSelectionsDoNotClaimCompleteCoverage()
    {
        var handler=new Api();using var http=new HttpClient(handler);var query=new GalaxyQueryService(http);var viewport=new GalaxyViewport(new(0,0,0),new(1,0,0),new(0,1,0),new(0,0,1),10,10,10);
        var none=new GalaxyMapFilter{Mode=GalaxyMapMode.Map,Category=GalaxyFilterCategory.Civilization,Values=new HashSet<string>{"none"}};
        var result=await query.QueryViewportAsync(none,viewport);Check.That(!result.CoverageComplete&&result.Truncated);Check.Equal(0,result.Systems.Count);
        var partial=none with{Category=GalaxyFilterCategory.Economy,Values=new HashSet<string>{"hightech","unpublished_original_option"}};
        result=await query.QueryViewportAsync(partial,viewport);Check.Equal(1,result.Systems.Count);Check.That(!result.CoverageComplete&&result.Truncated);
        var empty=partial with{Values=new HashSet<string>()};result=await query.QueryViewportAsync(empty,viewport);Check.That(result.CoverageComplete&&!result.Truncated);Check.Equal(0,result.Systems.Count);
    }
    [Test] public void ReportedStationListWithoutACarrierCannotProveNoCarrierExists()
    {
        var filter=new GalaxyMapFilter{Mode=GalaxyMapMode.Map,Category=GalaxyFilterCategory.FleetCarrier,Values=new HashSet<string>{"absent"}};
        var rows=Check.Json("""{"results":[{"name":"Reported","x":0,"y":0,"z":0,"stations":[{"type":"Coriolis Starport"}]}]}""");
        var parsed=GalaxyQueryService.Parse(rows,"systems",filter,false,new(0,0,0),10).Single();Check.That(parsed.Data.FleetCarrier is null);Check.That(!filter.Matches(parsed));
    }
    [Test] public void NewExplicitControllerCannotBeOverriddenByAnOlderQueriesPositiveEvidence()
    {
        var filter=new GalaxyMapFilter{Mode=GalaxyMapMode.Map,Category=GalaxyFilterCategory.Power,Values=new HashSet<string>{"oldcontroller"}};
        var old=new GalaxySystemData{Power="Old controller",ConfirmedQueries=new HashSet<string>{filter.QueryKey}};
        var system=new GalacticSystem("Changed control",new(0,0,0)){Data=old.Merge(new(){Power="New controller"})};
        Check.That(!filter.Matches(system));Check.That(!filter.Evaluate(system).Matches);
        var omitted=new GalacticSystem("Proven result",new(0,0,0)){Data=new(){ConfirmedQueries=new HashSet<string>{filter.QueryKey}}};Check.That(filter.Matches(omitted));
    }
    [Test] public void AStationsNewRecordedStateReplacesItsOldMarkerAndKeepsOtherStationsIndependent()
    {
        var filter=new GalaxyMapFilter{Mode=GalaxyMapMode.Map,Category=GalaxyFilterCategory.Markers,Values=new HashSet<string>{"repairing"}};
        GalaxySystemData Observation(string json)=>GalaxySystemData.FromJournal(Check.Json(json));
        var first=Observation("""{"StationName":"Port A","StationState":"UnderRepairs","timestamp":"2026-10-22T00:00:00Z"}""") with{ConfirmedQueries=new HashSet<string>{filter.QueryKey}};
        var updated=first.Merge(Observation("""{"StationName":"Port A","StationState":"Damaged","timestamp":"2026-10-23T00:00:00Z"}"""));
        Check.That(!filter.Matches(new("Measured",new(0,0,0)){Data=updated}));
        var historical=updated.Merge(first);Check.That(!filter.Matches(new("Measured",new(0,0,0)){Data=historical}));
        var second=updated.Merge(Observation("""{"StationName":"Port B","StationState":"UnderRepairs","timestamp":"2026-10-23T00:00:00Z"}"""));
        Check.That(filter.Matches(new("Measured",new(0,0,0)){Data=second}));Check.Equal(2,second.StationStates!.Count);
    }
    [Test] public async Task PublicPowerplayCandidatesStillRequireTheActualNativeTerritoryOwnerAndStage()
    {
        var handler=new Api();using var http=new HttpClient(handler);var query=new GalaxyQueryService(http);
        var filter=new GalaxyMapFilter{Mode=GalaxyMapMode.Powerplay,Category=GalaxyFilterCategory.Power,Values=new HashSet<string>{"actualcontroller"},PowerStateValues=new HashSet<string>{"stronghold"}};
        var plans=await query.PlansAsync(filter);Check.Equal(1,plans.Count);var fields=JsonSerializer.SerializeToElement(plans[0].Filters);
        Check.Equal("Actual controller",fields.GetProperty("controlling_power").GetProperty("value")[0].GetString());Check.Equal("Stronghold",fields.GetProperty("power_state").GetProperty("value")[0].GetString());
        var system=new GalacticSystem("Measured",new(0,0,0)){Data=new(){Power="Actual controller",PowerState="Stronghold",NativeFilterFacts=new("F1",42,100){PowerStatus=new(3,6,2,7,0,0,true),PowerTerritoryOwnerName="Actual controller"}}};Check.That(filter.Matches(system));
        Check.That(!filter.Matches(system with{Data=system.Data with{NativeFilterFacts=system.Data.NativeFilterFacts! with{PowerStatus=new(1,4,2,7,0,0,true)}}}));
        Check.That(!filter.Matches(system with{Data=system.Data with{NativeFilterFacts=system.Data.NativeFilterFacts! with{PowerTerritoryOwnerName="Other influence"}}}));
        var unknown=system with{Data=system.Data with{NativeFilterFacts=null}};Check.That(!filter.Matches(unknown));Check.That((filter with{IncludeUnknown=true}).Matches(unknown));
    }
    private sealed class Api:HttpMessageHandler
    {
        public readonly List<(string Url,JsonElement Json)> Requests=[];
        public readonly TaskCompletionSource SearchStarted=new(TaskCreationOptions.RunContinuationsAsynchronously);
        public bool SlowFirstSearch,DenseFirstSearch;public Coordinates? Point;private int searches;
        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,CancellationToken token)
        {
            var url=request.RequestUri!.AbsolutePath;var payload=request.Content is null?default:Check.Json(await request.Content.ReadAsStringAsync(CancellationToken.None));lock(Requests)Requests.Add((url,payload));
            string body;
            if(url.Contains("field_values"))body=url.EndsWith("/power_state")?"""{"values":["Unoccupied","Exploited","Fortified","Stronghold"]}""":url.EndsWith("/state")?"""{"values":["Construction","Damaged","DamagedHuman","UnderAttack","UnderRepairs"]}""":url.EndsWith("marketplace")?"""{"values":{"commodity":["Gold"]}}""":url.EndsWith("stations")?"""{"values":{"services":["Refuel","Restock","Material Trader"]}}""":"""{"values":["High Tech","Industrial","Actual controller"]}""";
            else
            {
                var sequence=Interlocked.Increment(ref searches);if(sequence==1&&SlowFirstSearch){SearchStarted.SetResult();await Task.Delay(100,CancellationToken.None);}
                body=url.Contains("/stations/")?"""{"count":1,"results":[{"system_name":"Measured","system_id64":1,"system_x":2,"system_y":3,"system_z":4,"services":[{"name":"Refuel"}],"material_trader":"Raw","market":[{"commodity":"Gold","supply":3}]}]}""":"""{"count":1,"results":[{"name":"Measured","id64":1,"x":2,"y":3,"z":4,"primary_economy":"High Tech","secondary_economy":"Industrial","controlling_power":"Actual controller","stations":[{"services":["Restock"]}]}]}""";
                if(DenseFirstSearch&&sequence==1)body=body.Replace("\"count\":1,","\"count\":1000,");
                if(Point is {} point)body=body.Replace("\"x\":2,\"y\":3,\"z\":4",FormattableString.Invariant($"\"x\":{point.X:R},\"y\":{point.Y:R},\"z\":{point.Z:R}"));
            }
            return new(HttpStatusCode.OK){Content=new StringContent(body,Encoding.UTF8,"application/json")};
        }
    }
}