using System.Globalization;
using System.IO.Pipes;
using System.Text.Json;
using XFEEDToolBox.Core.Galaxy;
using XFEExtension.NetCore.XUnit.Attributes;
namespace XFEEDToolBox.Test;
[TestFixture]
public sealed class NativeGalaxyCivilizationServicesWireTests
{
private const long First=9007199254740993;
private static object Civilization(string id="9007199254740995",string key="guardian",bool defaulted=false)=>new{definitionId=id,key,defaulted};
private static object Services(int mask=513,bool defaulted=false)=>new{mask,defaulted};
private static object Row(long address,params (string Name,object? Value)[] fields)
{
var row=new Dictionary<string,object?>{{"systemAddress",address.ToString(CultureInfo.InvariantCulture)}};
foreach(var field in fields)row.Add(field.Name,field.Value);
return row;
}
private static object Page(string origin,int total,bool done,params object[] rows)=>new
{generation=17UL,contextSystemAddress=origin,total,done,error="",items=rows};
private static async Task Serve(int pid,object result,CancellationToken token,Action<JsonElement>? inspect=null)
{
await using var server=new NamedPipeServerStream($"XFEEDToolBox.Navigation.{pid}",PipeDirection.InOut,1,PipeTransmissionMode.Byte,PipeOptions.Asynchronous);
await server.WaitForConnectionAsync(token);var buffer=new byte[8192];var count=0;
// The byte pipe has no message boundary. Accumulate a complete JSON
// request rather than assuming one ReadAsync returns every byte.
while(true)
{
if(count==buffer.Length)throw new InvalidDataException("Fixture request exceeds its bound.");
var read=await server.ReadAsync(buffer.AsMemory(count),token);
if(read==0)throw new EndOfStreamException();count+=read;
JsonDocument request;
try{request=JsonDocument.Parse(buffer.AsMemory(0,count));}catch(JsonException){continue;}
using(request)inspect?.Invoke(request.RootElement);
break;
}
await server.WriteAsync(JsonSerializer.SerializeToUtf8Bytes(new{ok=true,result}),token);
await server.WriteAsync(new byte[]{10},token);await server.FlushAsync(token);
if(OperatingSystem.IsWindows())server.WaitForPipeDrain();
}
private static void InspectRequest(JsonElement request,bool civilization,bool services)
{
Check.Equal("galaxy.facts",request.GetProperty("op").GetString());
Check.Equal(civilization,request.GetProperty("includeCivilization").GetBoolean());
Check.Equal(services,request.GetProperty("includeServices").GetBoolean());
Check.Equal(First.ToString(CultureInfo.InvariantCulture),request.GetProperty("systemAddresses")[0].GetString());
}
private static async Task<GalaxyNativeFilterFact> ReadOne(object row,bool civilization,bool services)
{
var pid=Random.Shared.Next(190000000,200000000);using var stop=new CancellationTokenSource(TimeSpan.FromSeconds(8));
var server=Task.Run(()=>Serve(pid,Page("42",1,true,row),stop.Token,r=>InspectRequest(r,civilization,services)));
var result=await new NativeGalaxyFilterFactsCatalog(pid).ReadAsync([First],stop.Token,includeCivilization:civilization,includeServices:services);
await server;Check.Equal(42L,result.OriginSystemAddress);return result.Rows.Single();
}
private static async Task RejectAndRecover(object row,bool civilization=true,bool services=true)
{
var pid=Random.Shared.Next(190000000,200000000);using var stop=new CancellationTokenSource(TimeSpan.FromSeconds(8));
var server=Task.Run(async()=>
{
await Serve(pid,Page("42",1,true,row),stop.Token,r=>InspectRequest(r,civilization,services));
await Serve(pid,new{cancelled=true},stop.Token,r=>
{Check.Equal("galaxy.facts.cancel",r.GetProperty("op").GetString());Check.Equal(17UL,r.GetProperty("generation").GetUInt64());});
await Serve(pid,Page("42",1,true,Row(First,("civilization",null),("services",null))),stop.Token,r=>InspectRequest(r,civilization,services));
});
var catalog=new NativeGalaxyFilterFactsCatalog(pid);var rejected=false;
try{await catalog.ReadAsync([First],stop.Token,includeCivilization:civilization,includeServices:services);}catch(InvalidDataException){rejected=true;}
Check.That(rejected,"Malformed or unsolicited classifier receipt was accepted.");
// Reusing the same client verifies both original-generation cleanup
// and release of the client's semaphore after rejection.
var recovered=await catalog.ReadAsync([First],stop.Token,includeCivilization:civilization,includeServices:services);
await server;var result=recovered.Rows.Single();
Check.That(result.Civilization is null&&result.ServiceMask is null&&result.ServicesDefaulted is null);
Check.Equal(civilization,result.CivilizationRequested);Check.Equal(services,result.ServicesRequested);
}
[Test] public async Task RequestedExplicitNullKeepsIndependentQueryBindingsWithoutInventingNegativeFacts()
{
foreach(var request in new[]{(Civilization:true,Services:false),(Civilization:false,Services:true),(Civilization:true,Services:true)})
{
var fact=await ReadOne(Row(First,("civilization",null),("services",null)),request.Civilization,request.Services);
Check.Equal(request.Civilization,fact.CivilizationRequested);Check.Equal(request.Services,fact.ServicesRequested);
Check.That(fact.Civilization is null&&fact.ServiceMask is null&&fact.ServicesDefaulted is null);
}
}
[Test] public async Task MissingRequestedFieldsAndUnsolicitedNonNullResultsAreRejectedWithCleanup()
{
await RejectAndRecover(Row(First,("services",Services())));
await RejectAndRecover(Row(First,("civilization",Civilization())));
await RejectAndRecover(Row(First,("civilization",Civilization()),("services",null)),civilization:false,services:true);
await RejectAndRecover(Row(First,("civilization",null),("services",Services())),civilization:true,services:false);
await RejectAndRecover(Row(First,("civilization",Civilization())),civilization:false,services:false);
await RejectAndRecover(Row(First,("services",Services())),civilization:false,services:false);
}
[Test] public async Task UnrequestedMissingOrExplicitNullFieldsNeverRetainPreviousClientResults()
{
var pid=Random.Shared.Next(190000000,200000000);using var stop=new CancellationTokenSource(TimeSpan.FromSeconds(8));
var server=Task.Run(async()=>
{
await Serve(pid,Page("42",1,true,Row(First,("civilization",Civilization()),("services",Services()))),stop.Token,r=>InspectRequest(r,true,true));
await Serve(pid,Page("42",1,true,Row(First)),stop.Token,r=>InspectRequest(r,false,false));
await Serve(pid,Page("42",1,true,Row(First,("civilization",null),("services",null))),stop.Token,r=>InspectRequest(r,false,false));
});
var catalog=new NativeGalaxyFilterFactsCatalog(pid);
var known=(await catalog.ReadAsync([First],stop.Token,includeCivilization:true,includeServices:true)).Rows.Single();
Check.That(known.Civilization?.Key=="guardian"&&known.ServiceMask==513);
for(var i=0;i<2;i++)
{
var fact=(await catalog.ReadAsync([First],stop.Token)).Rows.Single();
Check.That(!fact.CivilizationRequested&&!fact.ServicesRequested&&fact.Civilization is null&&fact.ServiceMask is null&&fact.ServicesDefaulted is null);
}
await server;
}
[Test] public async Task ServiceMaskAndDefaultedMustBeCompleteExactAndConsistent()
{
foreach(var invalid in new[]{"{}","{\"mask\":0}","{\"mask\":null,\"defaulted\":false}","{\"mask\":-1,\"defaulted\":false}",
"{\"mask\":1024,\"defaulted\":false}","{\"mask\":65536,\"defaulted\":false}","{\"mask\":1.5,\"defaulted\":false}",
"{\"mask\":\"1\",\"defaulted\":false}","{\"mask\":1,\"defaulted\":true}","{\"mask\":0,\"defaulted\":null}",
"{\"mask\":0,\"defaulted\":1}","{\"mask\":0,\"defaulted\":\"true\"}","[]","false"})
await RejectAndRecover(Row(First,("civilization",null),("services",Check.Json(invalid))));
foreach(var sample in new[]{(Mask:0,Defaulted:true),(Mask:0,Defaulted:false),(Mask:1023,Defaulted:false)})
{
var result=await ReadOne(Row(First,("services",Services(sample.Mask,sample.Defaulted))),false,true);
Check.Equal((ushort?)sample.Mask,result.ServiceMask);Check.Equal((bool?)sample.Defaulted,result.ServicesDefaulted);
Check.That(result.ServicesRequested&&!result.CivilizationRequested&&result.Civilization is null);
}
}
[Test] public async Task CivilizationDefinitionIdsStayU64AndInvalidDefaultsCannotBecomeNone()
{
foreach(var sample in new[]{(Id:"9007199254740995",Key:"guardian",Defaulted:false),(Id:"9223372036854775809",Key:"human",Defaulted:false),
(Id:"18446744073709551615",Key:"none",Defaulted:true)})
{
var result=await ReadOne(Row(First,("civilization",Civilization(sample.Id,sample.Key,sample.Defaulted))),true,false);
Check.Equal(ulong.Parse(sample.Id,CultureInfo.InvariantCulture),result.Civilization!.DefinitionId);
Check.Equal(sample.Key,result.Civilization.Key);Check.Equal(sample.Defaulted,result.Civilization.Defaulted);
Check.That(result.CivilizationRequested&&!result.ServicesRequested&&result.ServiceMask is null);
}
var future=await ReadOne(Row(First,("civilization",Civilization("9007199254740995","future"))),true,false);
Check.That(future.Civilization is{DefinitionId:9007199254740995UL,Key:null,Defaulted:false});
foreach(var invalid in new[]{"{}","{\"definitionId\":9007199254740995,\"key\":\"human\"}","{\"definitionId\":\"18446744073709551616\",\"key\":\"none\"}",
"{\"definitionId\":\"-1\",\"key\":\"none\"}","{\"definitionId\":\"1.0\",\"key\":\"human\"}","{\"definitionId\":null,\"key\":\"human\"}",
"{\"definitionId\":\"1\"}","{\"definitionId\":\"1\",\"key\":1}","{\"definitionId\":\"1\",\"key\":\"none\",\"defaulted\":true}",
"{\"definitionId\":\"18446744073709551615\",\"key\":\"human\",\"defaulted\":true}","{\"definitionId\":\"1\",\"key\":\"human\",\"defaulted\":\"true\"}","[]"})
await RejectAndRecover(Row(First,("civilization",Check.Json(invalid)),("services",null)));
}
[Test] public async Task ProgressivePagesKeepEachClassifierIndependentForEachAddress()
{
var pid=Random.Shared.Next(190000000,200000000);using var stop=new CancellationTokenSource(TimeSpan.FromSeconds(8));
var server=Task.Run(async()=>
{
await Serve(pid,Page("42",2,false,Row(First,("civilization",Civilization()),("services",null))),stop.Token,r=>InspectRequest(r,true,true));
await Serve(pid,Page("42",2,true,Row(First+1,("civilization",null),("services",Services(0,true)))),stop.Token,r=>
{Check.Equal("galaxy.facts.result",r.GetProperty("op").GetString());Check.Equal(1,r.GetProperty("offset").GetInt32());Check.Equal(17UL,r.GetProperty("generation").GetUInt64());});
});
var result=await new NativeGalaxyFilterFactsCatalog(pid).ReadAsync([First,First+1],stop.Token,includeCivilization:true,includeServices:true);await server;
Check.That(result.Rows.All(row=>row.CivilizationRequested&&row.ServicesRequested));
Check.That(result.Rows[0] is{SystemAddress:First,Civilization.Key:"guardian",ServiceMask:null,ServicesDefaulted:null});
Check.That(result.Rows[1] is{Civilization:null,ServiceMask:0,ServicesDefaulted:true});Check.Equal(First+1,result.Rows[1].SystemAddress);
}
[Test] public async Task ChangedOriginCannotPublishMixedClassifierPagesAndNextReadStartsClean()
{
var pid=Random.Shared.Next(190000000,200000000);using var stop=new CancellationTokenSource(TimeSpan.FromSeconds(8));
var server=Task.Run(async()=>
{
await Serve(pid,Page("42",2,false,Row(First,("civilization",Civilization()),("services",Services()))),stop.Token);
await Serve(pid,Page("43",2,true,Row(First+1,("civilization",Civilization(key:"human")),("services",Services(1023)))),stop.Token);
await Serve(pid,new{cancelled=true},stop.Token,r=>Check.Equal("galaxy.facts.cancel",r.GetProperty("op").GetString()));
await Serve(pid,Page("43",1,true,Row(First,("civilization",null),("services",null))),stop.Token);
});
var catalog=new NativeGalaxyFilterFactsCatalog(pid);var rejected=false;
try{await catalog.ReadAsync([First,First+1],stop.Token,includeCivilization:true,includeServices:true);}catch(InvalidDataException){rejected=true;}
Check.That(rejected);var fresh=await catalog.ReadAsync([First],stop.Token,includeCivilization:true,includeServices:true);await server;
Check.Equal(43L,fresh.OriginSystemAddress);Check.That(fresh.Rows.Single() is{Civilization:null,ServiceMask:null,ServicesDefaulted:null});
}
private static GalaxySystemData Data(GalaxyNativeFilterFact row,string account="F1",int pid=42,long origin=100)=>new()
{NativeFilterFacts=new(account,pid,origin){Civilization=row.Civilization,CivilizationRequested=row.CivilizationRequested,
ServiceMask=row.ServiceMask,ServicesRequested=row.ServicesRequested,ServicesDefaulted=row.ServicesDefaulted}};
[Test] public async Task IndependentWireUpdatesPreserveUnrequestedFactsButExplicitUnknownClearsThem()
{
var initial=await ReadOne(Row(First,("civilization",Civilization()),("services",Services(0,true))),true,true);
var civilizationOnly=await ReadOne(Row(First,("civilization",Civilization(key:"human"))),true,false);
var serviceOnly=await ReadOne(Row(First,("services",Services(513))),false,true);
var explicitUnknown=await ReadOne(Row(First,("civilization",null),("services",null)),true,true);
var state=Data(initial).Merge(Data(civilizationOnly));
Check.That(state.NativeFilterFacts is{CivilizationRequested:true,Civilization.Key:"human",ServicesRequested:true,ServiceMask:0,ServicesDefaulted:true});
state=state.Merge(Data(serviceOnly));
Check.That(state.NativeFilterFacts is{CivilizationRequested:true,Civilization.Key:"human",ServicesRequested:true,ServiceMask:513,ServicesDefaulted:false});
state=state.Merge(Data(explicitUnknown));
Check.That(state.NativeFilterFacts is{CivilizationRequested:true,Civilization:null,ServicesRequested:true,ServiceMask:null,ServicesDefaulted:null});
}
[Test] public async Task AccountProcessAndOriginResetsNeverCarryUnrequestedClassifierFacts()
{
var original=Data(await ReadOne(Row(First,("civilization",Civilization()),("services",Services())),true,true));
var unrelated=await ReadOne(Row(First),false,false);
foreach(var context in new[]{(Account:"F2",Pid:42,Origin:100L),(Account:"F1",Pid:43,Origin:100L),(Account:"F1",Pid:42,Origin:101L)})
{
var reset=original.ForNativeContext(context.Account,context.Pid,context.Origin);Check.That(reset.NativeFilterFacts is null);
var replaced=original.Merge(Data(unrelated,context.Account,context.Pid,context.Origin));
Check.That(replaced.NativeFilterFacts is{CivilizationRequested:false,Civilization:null,ServicesRequested:false,ServiceMask:null,ServicesDefaulted:null});
}
}
}
using System.Globalization;
using System.IO.Pipes;
using System.Text.Json;
using XFEEDToolBox.Core.Galaxy;
using XFEExtension.NetCore.XUnit.Attributes;
namespace XFEEDToolBox.Test;
[TestFixture]
public sealed class NativeGalaxyCivilizationServicesWireTests
{
private const long First=9007199254740993;
private static object Civilization(string id="9007199254740995",string key="guardian",bool defaulted=false)=>new{definitionId=id,key,defaulted};
private static object Services(int mask=513,bool defaulted=false)=>new{mask,defaulted};
private static object Row(long address,params (string Name,object? Value)[] fields)
{
var row=new Dictionary<string,object?>{{"systemAddress",address.ToString(CultureInfo.InvariantCulture)}};
foreach(var field in fields)row.Add(field.Name,field.Value);
return row;
}
private static object Page(string origin,int total,bool done,params object[] rows)=>new
{generation=17UL,contextSystemAddress=origin,total,done,error="",items=rows};
private static async Task Serve(int pid,object result,CancellationToken token,Action<JsonElement>? inspect=null)
{
await using var server=new NamedPipeServerStream($"XFEEDToolBox.Navigation.{pid}",PipeDirection.InOut,1,PipeTransmissionMode.Byte,PipeOptions.Asynchronous);
await server.WaitForConnectionAsync(token);var buffer=new byte[8192];var count=0;
// The byte pipe has no message boundary. Accumulate a complete JSON
// request rather than assuming one ReadAsync returns every byte.
while(true)
{
if(count==buffer.Length)throw new InvalidDataException("Fixture request exceeds its bound.");
var read=await server.ReadAsync(buffer.AsMemory(count),token);
if(read==0)throw new EndOfStreamException();count+=read;
JsonDocument request;
try{request=JsonDocument.Parse(buffer.AsMemory(0,count));}catch(JsonException){continue;}
using(request)inspect?.Invoke(request.RootElement);
break;
}
await server.WriteAsync(JsonSerializer.SerializeToUtf8Bytes(new{ok=true,result}),token);
await server.WriteAsync(new byte[]{10},token);await server.FlushAsync(token);
if(OperatingSystem.IsWindows())server.WaitForPipeDrain();
}
private static void InspectRequest(JsonElement request,bool civilization,bool services)
{
Check.Equal("galaxy.facts",request.GetProperty("op").GetString());
Check.Equal(civilization,request.GetProperty("includeCivilization").GetBoolean());
Check.Equal(services,request.GetProperty("includeServices").GetBoolean());
Check.Equal(First.ToString(CultureInfo.InvariantCulture),request.GetProperty("systemAddresses")[0].GetString());
}
private static async Task<GalaxyNativeFilterFact> ReadOne(object row,bool civilization,bool services)
{
var pid=Random.Shared.Next(190000000,200000000);using var stop=new CancellationTokenSource(TimeSpan.FromSeconds(8));
var server=Task.Run(()=>Serve(pid,Page("42",1,true,row),stop.Token,r=>InspectRequest(r,civilization,services)));
var result=await new NativeGalaxyFilterFactsCatalog(pid).ReadAsync([First],stop.Token,includeCivilization:civilization,includeServices:services);
await server;Check.Equal(42L,result.OriginSystemAddress);return result.Rows.Single();
}
private static async Task RejectAndRecover(object row,bool civilization=true,bool services=true)
{
var pid=Random.Shared.Next(190000000,200000000);using var stop=new CancellationTokenSource(TimeSpan.FromSeconds(8));
var server=Task.Run(async()=>
{
await Serve(pid,Page("42",1,true,row),stop.Token,r=>InspectRequest(r,civilization,services));
await Serve(pid,new{cancelled=true},stop.Token,r=>
{Check.Equal("galaxy.facts.cancel",r.GetProperty("op").GetString());Check.Equal(17UL,r.GetProperty("generation").GetUInt64());});
await Serve(pid,Page("42",1,true,Row(First,("civilization",null),("services",null))),stop.Token,r=>InspectRequest(r,civilization,services));
});
var catalog=new NativeGalaxyFilterFactsCatalog(pid);var rejected=false;
try{await catalog.ReadAsync([First],stop.Token,includeCivilization:civilization,includeServices:services);}catch(InvalidDataException){rejected=true;}
Check.That(rejected,"Malformed or unsolicited classifier receipt was accepted.");
// Reusing the same client verifies both original-generation cleanup
// and release of the client's semaphore after rejection.
var recovered=await catalog.ReadAsync([First],stop.Token,includeCivilization:civilization,includeServices:services);
await server;var result=recovered.Rows.Single();
Check.That(result.Civilization is null&&result.ServiceMask is null&&result.ServicesDefaulted is null);
Check.Equal(civilization,result.CivilizationRequested);Check.Equal(services,result.ServicesRequested);
}
[Test] public async Task RequestedExplicitNullKeepsIndependentQueryBindingsWithoutInventingNegativeFacts()
{
foreach(var request in new[]{(Civilization:true,Services:false),(Civilization:false,Services:true),(Civilization:true,Services:true)})
{
var fact=await ReadOne(Row(First,("civilization",null),("services",null)),request.Civilization,request.Services);
Check.Equal(request.Civilization,fact.CivilizationRequested);Check.Equal(request.Services,fact.ServicesRequested);
Check.That(fact.Civilization is null&&fact.ServiceMask is null&&fact.ServicesDefaulted is null);
}
}
[Test] public async Task MissingRequestedFieldsAndUnsolicitedNonNullResultsAreRejectedWithCleanup()
{
await RejectAndRecover(Row(First,("services",Services())));
await RejectAndRecover(Row(First,("civilization",Civilization())));
await RejectAndRecover(Row(First,("civilization",Civilization()),("services",null)),civilization:false,services:true);
await RejectAndRecover(Row(First,("civilization",null),("services",Services())),civilization:true,services:false);
await RejectAndRecover(Row(First,("civilization",Civilization())),civilization:false,services:false);
await RejectAndRecover(Row(First,("services",Services())),civilization:false,services:false);
}
[Test] public async Task UnrequestedMissingOrExplicitNullFieldsNeverRetainPreviousClientResults()
{
var pid=Random.Shared.Next(190000000,200000000);using var stop=new CancellationTokenSource(TimeSpan.FromSeconds(8));
var server=Task.Run(async()=>
{
await Serve(pid,Page("42",1,true,Row(First,("civilization",Civilization()),("services",Services()))),stop.Token,r=>InspectRequest(r,true,true));
await Serve(pid,Page("42",1,true,Row(First)),stop.Token,r=>InspectRequest(r,false,false));
await Serve(pid,Page("42",1,true,Row(First,("civilization",null),("services",null))),stop.Token,r=>InspectRequest(r,false,false));
});
var catalog=new NativeGalaxyFilterFactsCatalog(pid);
var known=(await catalog.ReadAsync([First],stop.Token,includeCivilization:true,includeServices:true)).Rows.Single();
Check.That(known.Civilization?.Key=="guardian"&&known.ServiceMask==513);
for(var i=0;i<2;i++)
{
var fact=(await catalog.ReadAsync([First],stop.Token)).Rows.Single();
Check.That(!fact.CivilizationRequested&&!fact.ServicesRequested&&fact.Civilization is null&&fact.ServiceMask is null&&fact.ServicesDefaulted is null);
}
await server;
}
[Test] public async Task ServiceMaskAndDefaultedMustBeCompleteExactAndConsistent()
{
foreach(var invalid in new[]{"{}","{\"mask\":0}","{\"mask\":null,\"defaulted\":false}","{\"mask\":-1,\"defaulted\":false}",
"{\"mask\":1024,\"defaulted\":false}","{\"mask\":65536,\"defaulted\":false}","{\"mask\":1.5,\"defaulted\":false}",
"{\"mask\":\"1\",\"defaulted\":false}","{\"mask\":1,\"defaulted\":true}","{\"mask\":0,\"defaulted\":null}",
"{\"mask\":0,\"defaulted\":1}","{\"mask\":0,\"defaulted\":\"true\"}","[]","false"})
await RejectAndRecover(Row(First,("civilization",null),("services",Check.Json(invalid))));
foreach(var sample in new[]{(Mask:0,Defaulted:true),(Mask:0,Defaulted:false),(Mask:1023,Defaulted:false)})
{
var result=await ReadOne(Row(First,("services",Services(sample.Mask,sample.Defaulted))),false,true);
Check.Equal((ushort?)sample.Mask,result.ServiceMask);Check.Equal((bool?)sample.Defaulted,result.ServicesDefaulted);
Check.That(result.ServicesRequested&&!result.CivilizationRequested&&result.Civilization is null);
}
}
[Test] public async Task CivilizationDefinitionIdsStayU64AndInvalidDefaultsCannotBecomeNone()
{
foreach(var sample in new[]{(Id:"9007199254740995",Key:"guardian",Defaulted:false),(Id:"9223372036854775809",Key:"human",Defaulted:false),
(Id:"18446744073709551615",Key:"none",Defaulted:true)})
{
var result=await ReadOne(Row(First,("civilization",Civilization(sample.Id,sample.Key,sample.Defaulted))),true,false);
Check.Equal(ulong.Parse(sample.Id,CultureInfo.InvariantCulture),result.Civilization!.DefinitionId);
Check.Equal(sample.Key,result.Civilization.Key);Check.Equal(sample.Defaulted,result.Civilization.Defaulted);
Check.That(result.CivilizationRequested&&!result.ServicesRequested&&result.ServiceMask is null);
}
var future=await ReadOne(Row(First,("civilization",Civilization("9007199254740995","future"))),true,false);
Check.That(future.Civilization is{DefinitionId:9007199254740995UL,Key:null,Defaulted:false});
foreach(var invalid in new[]{"{}","{\"definitionId\":9007199254740995,\"key\":\"human\"}","{\"definitionId\":\"18446744073709551616\",\"key\":\"none\"}",
"{\"definitionId\":\"-1\",\"key\":\"none\"}","{\"definitionId\":\"1.0\",\"key\":\"human\"}","{\"definitionId\":null,\"key\":\"human\"}",
"{\"definitionId\":\"1\"}","{\"definitionId\":\"1\",\"key\":1}","{\"definitionId\":\"1\",\"key\":\"none\",\"defaulted\":true}",
"{\"definitionId\":\"18446744073709551615\",\"key\":\"human\",\"defaulted\":true}","{\"definitionId\":\"1\",\"key\":\"human\",\"defaulted\":\"true\"}","[]"})
await RejectAndRecover(Row(First,("civilization",Check.Json(invalid)),("services",null)));
}
[Test] public async Task ProgressivePagesKeepEachClassifierIndependentForEachAddress()
{
var pid=Random.Shared.Next(190000000,200000000);using var stop=new CancellationTokenSource(TimeSpan.FromSeconds(8));
var server=Task.Run(async()=>
{
await Serve(pid,Page("42",2,false,Row(First,("civilization",Civilization()),("services",null))),stop.Token,r=>InspectRequest(r,true,true));
await Serve(pid,Page("42",2,true,Row(First+1,("civilization",null),("services",Services(0,true)))),stop.Token,r=>
{Check.Equal("galaxy.facts.result",r.GetProperty("op").GetString());Check.Equal(1,r.GetProperty("offset").GetInt32());Check.Equal(17UL,r.GetProperty("generation").GetUInt64());});
});
var result=await new NativeGalaxyFilterFactsCatalog(pid).ReadAsync([First,First+1],stop.Token,includeCivilization:true,includeServices:true);await server;
Check.That(result.Rows.All(row=>row.CivilizationRequested&&row.ServicesRequested));
Check.That(result.Rows[0] is{SystemAddress:First,Civilization.Key:"guardian",ServiceMask:null,ServicesDefaulted:null});
Check.That(result.Rows[1] is{Civilization:null,ServiceMask:0,ServicesDefaulted:true});Check.Equal(First+1,result.Rows[1].SystemAddress);
}
[Test] public async Task ChangedOriginCannotPublishMixedClassifierPagesAndNextReadStartsClean()
{
var pid=Random.Shared.Next(190000000,200000000);using var stop=new CancellationTokenSource(TimeSpan.FromSeconds(8));
var server=Task.Run(async()=>
{
await Serve(pid,Page("42",2,false,Row(First,("civilization",Civilization()),("services",Services()))),stop.Token);
await Serve(pid,Page("43",2,true,Row(First+1,("civilization",Civilization(key:"human")),("services",Services(1023)))),stop.Token);
await Serve(pid,new{cancelled=true},stop.Token,r=>Check.Equal("galaxy.facts.cancel",r.GetProperty("op").GetString()));
await Serve(pid,Page("43",1,true,Row(First,("civilization",null),("services",null))),stop.Token);
});
var catalog=new NativeGalaxyFilterFactsCatalog(pid);var rejected=false;
try{await catalog.ReadAsync([First,First+1],stop.Token,includeCivilization:true,includeServices:true);}catch(InvalidDataException){rejected=true;}
Check.That(rejected);var fresh=await catalog.ReadAsync([First],stop.Token,includeCivilization:true,includeServices:true);await server;
Check.Equal(43L,fresh.OriginSystemAddress);Check.That(fresh.Rows.Single() is{Civilization:null,ServiceMask:null,ServicesDefaulted:null});
}
private static GalaxySystemData Data(GalaxyNativeFilterFact row,string account="F1",int pid=42,long origin=100)=>new()
{NativeFilterFacts=new(account,pid,origin){Civilization=row.Civilization,CivilizationRequested=row.CivilizationRequested,
ServiceMask=row.ServiceMask,ServicesRequested=row.ServicesRequested,ServicesDefaulted=row.ServicesDefaulted}};
[Test] public async Task IndependentWireUpdatesPreserveUnrequestedFactsButExplicitUnknownClearsThem()
{
var initial=await ReadOne(Row(First,("civilization",Civilization()),("services",Services(0,true))),true,true);
var civilizationOnly=await ReadOne(Row(First,("civilization",Civilization(key:"human"))),true,false);
var serviceOnly=await ReadOne(Row(First,("services",Services(513))),false,true);
var explicitUnknown=await ReadOne(Row(First,("civilization",null),("services",null)),true,true);
var state=Data(initial).Merge(Data(civilizationOnly));
Check.That(state.NativeFilterFacts is{CivilizationRequested:true,Civilization.Key:"human",ServicesRequested:true,ServiceMask:0,ServicesDefaulted:true});
state=state.Merge(Data(serviceOnly));
Check.That(state.NativeFilterFacts is{CivilizationRequested:true,Civilization.Key:"human",ServicesRequested:true,ServiceMask:513,ServicesDefaulted:false});
state=state.Merge(Data(explicitUnknown));
Check.That(state.NativeFilterFacts is{CivilizationRequested:true,Civilization:null,ServicesRequested:true,ServiceMask:null,ServicesDefaulted:null});
}
[Test] public async Task AccountProcessAndOriginResetsNeverCarryUnrequestedClassifierFacts()
{
var original=Data(await ReadOne(Row(First,("civilization",Civilization()),("services",Services())),true,true));
var unrelated=await ReadOne(Row(First),false,false);
foreach(var context in new[]{(Account:"F2",Pid:42,Origin:100L),(Account:"F1",Pid:43,Origin:100L),(Account:"F1",Pid:42,Origin:101L)})
{
var reset=original.ForNativeContext(context.Account,context.Pid,context.Origin);Check.That(reset.NativeFilterFacts is null);
var replaced=original.Merge(Data(unrelated,context.Account,context.Pid,context.Origin));
Check.That(replaced.NativeFilterFacts is{CivilizationRequested:false,Civilization:null,ServicesRequested:false,ServiceMask:null,ServicesDefaulted:null});
}
}
}