using XFEExtension.NetCore.XUnit.Attributes;
using XFEEDToolBox.Core.Data;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Native;
namespace XFEEDToolBox.Test;
[TestFixture]
public class FleetTests
{
private const string Loadout = """
{"timestamp":"2026-10-21T01:00:00Z","event":"Loadout","Ship":"mandalay","ShipID":3,"ShipName":"TEST","ShipIdent":"T-01","CargoCapacity":8,"MaxJumpRange":66.715927,"Rebuy":3489668,"FuelCapacity":{"Main":32,"Reserve":0.77},"Modules":[
{"Slot":"MainEngines","Item":"int_engine_size5_class5","On":true,"Priority":0,"Health":0.96,"Value":5103960,"Engineering":{"Engineer":"Test Engineer","BlueprintName":"Engine_Dirty","Level":3,"Quality":1.0,"ExperimentalEffect":"test_effect","Modifiers":[{"Label":"Integrity","Value":96.459999,"OriginalValue":106,"LessIsGood":0},{"Label":"PowerDraw","Value":6.6096,"OriginalValue":6.12,"LessIsGood":1}]}},
{"Slot":"Slot10","Item":"int_dockingcomputer_advanced","On":true,"Priority":1,"Health":1},
{"Slot":"Slot11","Item":"int_supercruiseassist","On":false,"Priority":1,"Health":1}
]}
""";
[Test] public void LoadoutPreservesSlotsEngineeringAndAssistState()
{
var p = new PlayerProjector(); p.Apply(Check.Json(Loadout)); var s = p.State.Ships.Single();
Check.Equal(3L, p.State.CurrentShipId!.Value); Check.Equal(8, s.CargoCapacity!.Value); Check.Equal(32d, s.FuelMain!.Value);
Check.Equal(3, s.Modules.Count); var engine = s.Modules[0]; Check.Equal("MainEngines", engine.Slot); Check.Equal(0, engine.Priority);
Check.Equal("Engine_Dirty", engine.Engineering!.Blueprint); Check.Equal(3, engine.Engineering.Level); Check.Equal(106d, engine.Engineering.Modifiers[0].OriginalValue!.Value);
Check.Equal(96.459999d, engine.Engineering.Modifiers[0].Value!.Value); Check.That(engine.Engineering.Modifiers[1].LessIsGood);
Check.That(p.State.AdvancedDocking && !p.State.SupercruiseAssist);
}
[Test] public void StoredShipsRetainsKnownConfigurationWithoutInventingRemoteModules()
{
var p = new PlayerProjector(); p.Apply(Check.Json(Loadout));
p.Apply(Check.Json("""{"timestamp":"2026-10-21T01:01:00Z","event":"StoredShips","StarSystem":"Vaka","MarketID":10,"ShipsHere":[{"ShipID":3,"ShipType":"mandalay","Value":100,"Hot":false}],"ShipsRemote":[{"ShipID":7,"ShipType":"python","Name":"Remote","StarSystem":"Sol","ShipMarketID":20,"TransferPrice":123,"TransferTime":300,"Value":500}]}"""));
Check.Equal(2, p.State.Ships.Count); var remote = p.State.Ships.Single(x => x.ShipId == 7); Check.Equal("Sol", remote.SystemName); Check.Equal(20L, remote.MarketId!.Value);
Check.That(remote.ConfigurationAt == null && remote.Modules.Count == 0); Check.Equal(3, p.State.Ships.Single(x => x.ShipId == 3).Modules.Count);
p.Apply(Check.Json("""{"event":"ShipyardSell","SellShipID":7}""")); Check.Equal(1, p.State.Ships.Count);
}
[Test] public void ModuleChangeNeedsNewerFullLoadout()
{
var p = new PlayerProjector(); p.Apply(Check.Json(Loadout));
p.Apply(Check.Json("""{"timestamp":"2026-10-21T01:02:00Z","event":"ModuleSwap","ShipID":3}"""));
p.Apply(Check.Json(Loadout)); Check.That(p.State.Ships.Single().ConfigurationChanged);
p.Apply(Check.Json(Loadout.Replace("01:00:00Z", "01:03:00Z"))); Check.That(!p.State.Ships.Single().ConfigurationChanged);
}
[Test] public void EmptyStoredModulesIsObservedAndOldSnapshotCannotRestoreItems()
{
var p = new PlayerProjector();
p.Apply(Check.Json("""{"timestamp":"2026-10-21T01:00:00Z","event":"StoredModules","Items":[{"Name":"engine","Name_Localised":"Engine","StorageSlot":4,"StarSystem":"Sol","MarketID":1,"BuyPrice":100,"TransferCost":20,"TransferTime":60,"EngineerModifications":"Engine_Dirty","Level":3,"Quality":1}]}"""));
Check.Equal("Engine_Dirty", p.State.StoredModules.Single().Engineering); Check.Equal(3, p.State.StoredModules.Single().EngineeringLevel!.Value);
p.Apply(Check.Json("""{"timestamp":"2026-10-21T01:02:00Z","event":"StoredModules","Items":[]}"""));
p.Apply(Check.Json("""{"timestamp":"2026-10-21T01:01:00Z","event":"StoredModules","Items":[{"Name":"stale"}]}"""));
Check.Equal(0, p.State.StoredModules.Count); Check.That(p.State.StoredModulesAt != null);
Check.Equal("已读取", ReadAcceptance.Build(p.State, null).Single(x => x.Category == "寄存模块").Status);
}
[Test] public void AccountChangeClearsFleetAndStoredModules()
{
var p = new PlayerProjector(); p.Apply(Check.Json("""{"event":"LoadGame","FID":"a"}""")); p.Apply(Check.Json(Loadout));
p.Apply(Check.Json("""{"event":"StoredModules","Items":[{"Name":"test"}]}"""));
p.Apply(Check.Json("""{"event":"LoadGame","FID":"b","ShipID":0}"""));
Check.Equal(0, p.State.Ships.Count); Check.Equal(0, p.State.StoredModules.Count); Check.That(p.State.StoredModulesAt == null);
}
[Test] public void MaterialConsumptionAndOldSnapshotDoNotRestoreSpentIngredients()
{
var p = new PlayerProjector();
p.Apply(Check.Json("""{"timestamp":"2026-10-21T01:00:00Z","event":"Materials","Raw":[{"Name":"iron","Count":8}],"Manufactured":[],"Encoded":[]}"""));
p.Apply(Check.Json("""{"timestamp":"2026-10-21T01:01:00Z","event":"EngineerCraft","Ingredients":[{"Name":"iron","Count":3}]}"""));
p.Apply(Check.Json("""{"timestamp":"2026-10-21T01:00:00Z","event":"Materials","Raw":[{"Name":"iron","Count":8}]}"""));
Check.Equal(5, p.State.Materials.Single().Count); Check.Equal("Raw", p.State.Materials.Single().Group); Check.That(p.State.MaterialsReliable);
}
[Test] public void MissingBackpackAndEmptyBackpackHaveDifferentCoverage()
{
var p = new PlayerProjector();
Check.Equal("缺少快照", ReadAcceptance.Build(p.State, null).Single(x => x.Category == "随身背包").Status);
p.Apply(Check.Json("""{"timestamp":"2026-10-21T01:00:00Z","event":"Backpack","Items":[],"Components":[],"Consumables":[],"Data":[]}"""));
Check.Equal("已读取", ReadAcceptance.Build(p.State, null).Single(x => x.Category == "随身背包").Status);
p.Apply(Check.Json("""{"event":"TransferMicroResources"}""")); Check.That(!p.State.BackpackReliable);
}
[Test] public async Task NativeResearchRejectsStandardGameBeforeStartingAnyHelper()
{
await using var bridge = new NativeBridgeClient();
try { await bridge.ConnectAsync("must-not-execute", "must-not-execute", new() { ProcessId = 1, ImageSha256 = new string('a', 64), ProfileMode = "standard" }); }
catch (InvalidOperationException e) { Check.That(e.Message.Contains("禁止连接在线客户端")); Check.That(!bridge.Connected); return; }
throw new InvalidOperationException("Standard game environment was not rejected.");
}
}
using XFEExtension.NetCore.XUnit.Attributes;
using XFEEDToolBox.Core.Data;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Native;
namespace XFEEDToolBox.Test;
[TestFixture]
public class FleetTests
{
private const string Loadout = """
{"timestamp":"2026-10-21T01:00:00Z","event":"Loadout","Ship":"mandalay","ShipID":3,"ShipName":"TEST","ShipIdent":"T-01","CargoCapacity":8,"MaxJumpRange":66.715927,"Rebuy":3489668,"FuelCapacity":{"Main":32,"Reserve":0.77},"Modules":[
{"Slot":"MainEngines","Item":"int_engine_size5_class5","On":true,"Priority":0,"Health":0.96,"Value":5103960,"Engineering":{"Engineer":"Test Engineer","BlueprintName":"Engine_Dirty","Level":3,"Quality":1.0,"ExperimentalEffect":"test_effect","Modifiers":[{"Label":"Integrity","Value":96.459999,"OriginalValue":106,"LessIsGood":0},{"Label":"PowerDraw","Value":6.6096,"OriginalValue":6.12,"LessIsGood":1}]}},
{"Slot":"Slot10","Item":"int_dockingcomputer_advanced","On":true,"Priority":1,"Health":1},
{"Slot":"Slot11","Item":"int_supercruiseassist","On":false,"Priority":1,"Health":1}
]}
""";
[Test] public void LoadoutPreservesSlotsEngineeringAndAssistState()
{
var p = new PlayerProjector(); p.Apply(Check.Json(Loadout)); var s = p.State.Ships.Single();
Check.Equal(3L, p.State.CurrentShipId!.Value); Check.Equal(8, s.CargoCapacity!.Value); Check.Equal(32d, s.FuelMain!.Value);
Check.Equal(3, s.Modules.Count); var engine = s.Modules[0]; Check.Equal("MainEngines", engine.Slot); Check.Equal(0, engine.Priority);
Check.Equal("Engine_Dirty", engine.Engineering!.Blueprint); Check.Equal(3, engine.Engineering.Level); Check.Equal(106d, engine.Engineering.Modifiers[0].OriginalValue!.Value);
Check.Equal(96.459999d, engine.Engineering.Modifiers[0].Value!.Value); Check.That(engine.Engineering.Modifiers[1].LessIsGood);
Check.That(p.State.AdvancedDocking && !p.State.SupercruiseAssist);
}
[Test] public void StoredShipsRetainsKnownConfigurationWithoutInventingRemoteModules()
{
var p = new PlayerProjector(); p.Apply(Check.Json(Loadout));
p.Apply(Check.Json("""{"timestamp":"2026-10-21T01:01:00Z","event":"StoredShips","StarSystem":"Vaka","MarketID":10,"ShipsHere":[{"ShipID":3,"ShipType":"mandalay","Value":100,"Hot":false}],"ShipsRemote":[{"ShipID":7,"ShipType":"python","Name":"Remote","StarSystem":"Sol","ShipMarketID":20,"TransferPrice":123,"TransferTime":300,"Value":500}]}"""));
Check.Equal(2, p.State.Ships.Count); var remote = p.State.Ships.Single(x => x.ShipId == 7); Check.Equal("Sol", remote.SystemName); Check.Equal(20L, remote.MarketId!.Value);
Check.That(remote.ConfigurationAt == null && remote.Modules.Count == 0); Check.Equal(3, p.State.Ships.Single(x => x.ShipId == 3).Modules.Count);
p.Apply(Check.Json("""{"event":"ShipyardSell","SellShipID":7}""")); Check.Equal(1, p.State.Ships.Count);
}
[Test] public void ModuleChangeNeedsNewerFullLoadout()
{
var p = new PlayerProjector(); p.Apply(Check.Json(Loadout));
p.Apply(Check.Json("""{"timestamp":"2026-10-21T01:02:00Z","event":"ModuleSwap","ShipID":3}"""));
p.Apply(Check.Json(Loadout)); Check.That(p.State.Ships.Single().ConfigurationChanged);
p.Apply(Check.Json(Loadout.Replace("01:00:00Z", "01:03:00Z"))); Check.That(!p.State.Ships.Single().ConfigurationChanged);
}
[Test] public void EmptyStoredModulesIsObservedAndOldSnapshotCannotRestoreItems()
{
var p = new PlayerProjector();
p.Apply(Check.Json("""{"timestamp":"2026-10-21T01:00:00Z","event":"StoredModules","Items":[{"Name":"engine","Name_Localised":"Engine","StorageSlot":4,"StarSystem":"Sol","MarketID":1,"BuyPrice":100,"TransferCost":20,"TransferTime":60,"EngineerModifications":"Engine_Dirty","Level":3,"Quality":1}]}"""));
Check.Equal("Engine_Dirty", p.State.StoredModules.Single().Engineering); Check.Equal(3, p.State.StoredModules.Single().EngineeringLevel!.Value);
p.Apply(Check.Json("""{"timestamp":"2026-10-21T01:02:00Z","event":"StoredModules","Items":[]}"""));
p.Apply(Check.Json("""{"timestamp":"2026-10-21T01:01:00Z","event":"StoredModules","Items":[{"Name":"stale"}]}"""));
Check.Equal(0, p.State.StoredModules.Count); Check.That(p.State.StoredModulesAt != null);
Check.Equal("已读取", ReadAcceptance.Build(p.State, null).Single(x => x.Category == "寄存模块").Status);
}
[Test] public void AccountChangeClearsFleetAndStoredModules()
{
var p = new PlayerProjector(); p.Apply(Check.Json("""{"event":"LoadGame","FID":"a"}""")); p.Apply(Check.Json(Loadout));
p.Apply(Check.Json("""{"event":"StoredModules","Items":[{"Name":"test"}]}"""));
p.Apply(Check.Json("""{"event":"LoadGame","FID":"b","ShipID":0}"""));
Check.Equal(0, p.State.Ships.Count); Check.Equal(0, p.State.StoredModules.Count); Check.That(p.State.StoredModulesAt == null);
}
[Test] public void MaterialConsumptionAndOldSnapshotDoNotRestoreSpentIngredients()
{
var p = new PlayerProjector();
p.Apply(Check.Json("""{"timestamp":"2026-10-21T01:00:00Z","event":"Materials","Raw":[{"Name":"iron","Count":8}],"Manufactured":[],"Encoded":[]}"""));
p.Apply(Check.Json("""{"timestamp":"2026-10-21T01:01:00Z","event":"EngineerCraft","Ingredients":[{"Name":"iron","Count":3}]}"""));
p.Apply(Check.Json("""{"timestamp":"2026-10-21T01:00:00Z","event":"Materials","Raw":[{"Name":"iron","Count":8}]}"""));
Check.Equal(5, p.State.Materials.Single().Count); Check.Equal("Raw", p.State.Materials.Single().Group); Check.That(p.State.MaterialsReliable);
}
[Test] public void MissingBackpackAndEmptyBackpackHaveDifferentCoverage()
{
var p = new PlayerProjector();
Check.Equal("缺少快照", ReadAcceptance.Build(p.State, null).Single(x => x.Category == "随身背包").Status);
p.Apply(Check.Json("""{"timestamp":"2026-10-21T01:00:00Z","event":"Backpack","Items":[],"Components":[],"Consumables":[],"Data":[]}"""));
Check.Equal("已读取", ReadAcceptance.Build(p.State, null).Single(x => x.Category == "随身背包").Status);
p.Apply(Check.Json("""{"event":"TransferMicroResources"}""")); Check.That(!p.State.BackpackReliable);
}
[Test] public async Task NativeResearchRejectsStandardGameBeforeStartingAnyHelper()
{
await using var bridge = new NativeBridgeClient();
try { await bridge.ConnectAsync("must-not-execute", "must-not-execute", new() { ProcessId = 1, ImageSha256 = new string('a', 64), ProfileMode = "standard" }); }
catch (InvalidOperationException e) { Check.That(e.Message.Contains("禁止连接在线客户端")); Check.That(!bridge.Connected); return; }
throw new InvalidOperationException("Standard game environment was not rejected.");
}
}