using XFEExtension.NetCore.XUnit.Attributes;
using XFEEDToolBox.Core.Data;
using XFEEDToolBox.Core.Engineering;
using XFEEDToolBox.Core.Models;
namespace XFEEDToolBox.Test;
[TestFixture]
public class EngineeringTests
{
private static readonly EngineeringCatalog Catalog = EngineeringCatalog.Default;
private static FittedModule Module(ModuleEngineering? engineering = null) => new() { Slot = "LargeHardpoint1", Item = "hpt_multicannon_gimbal_large", Engineering = engineering };
private static EngineerAccess Tod(int rank, double reputation) => new() { Id = 300260, Name = "Tod 'The Blaster' McQuinn", Rank = rank, Progress = "Unlocked", ReputationMin = reputation, ReputationMax = reputation };
private static EngineeringPlan Plan(FittedModule m, EngineerAccess e, int grade = 5, double quality = 1, EngineeringRecipe? effect = null, bool remote = false) => EngineeringPlanner.Calculate(Catalog, m, "Overcharged Weapon", grade, quality, effect, e, [], remote);
[Test] public void CapturedRulesPreserveOverflowAndValidateThresholds()
{
var r = EngineeringRules.Captured; Check.Equal(4, r.Rank(99.99)); Check.Equal(5, r.Rank(100)); Check.Equal(5.4, r.Reward(3)); Check.Equal(80d, r.ProgressPercent);
Check.Equal(60.5, r.Absolute(4, 50));
}
[Test] public void RankFourPromotesWithinGradeAndReducesRemainingRolls()
{
var m = Module(new() { Blueprint = "Weapon_Overcharged", Level = 3, Quality = .2 });
var p = Plan(m, Tod(4, 94.6), 3);
Check.Equal(3, p.Rolls.Count); Check.Equal(4, p.Rolls[0].BeforeRank); Check.Equal(5, p.Rolls[0].AfterRank);
Check.Equal(5, p.Rolls[1].BeforeRank); Check.That(p.Rolls[1].AfterQuality - p.Rolls[1].BeforeQuality > .33);
Check.That(p.Complete && p.Exact); Check.Equal(3, p.Materials.Single(x => x.Key == "nickel").Required);
}
[Test] public void FreshEngineerLevelsDynamicallyToFiveDuringOneModule()
{
var p = Plan(Module(), Tod(1, 0)); Check.That(p.Complete && p.FinalRank == 5);
Check.Equal(4, p.Rolls.Count(x => x.Grade == 1)); Check.Equal(5, p.Rolls.Count(x => x.Grade == 5));
Check.That(p.Rolls.Any(x => x.BeforeRank == 4 && x.AfterRank == 5));
Check.That(p.Rolls.Any(x => x.BeforeReputation < 1 && x.AfterReputation > 1));
}
[Test] public void MaxRankUsesOneTwoThreeFourFiveRolls()
{
var p = Plan(Module(), Tod(5, 100)); Check.Equal(15, p.Rolls.Count);
for (int grade = 1; grade <= 5; grade++) Check.Equal(grade, p.Rolls.Count(x => x.Grade == grade));
}
[Test] public void PartialGradeCountsOnlyRemainingProgressAndExperimentalOnce()
{
var effect = Catalog.Recipes.Single(r => r.Type == "Multi-cannon" && r.Name == "Incendiary Rounds" && r.Grade == 0);
var p = Plan(Module(new() { Blueprint = "Weapon_Overcharged", Level = 5, Quality = .6 }), Tod(5, 100), effect: effect);
Check.Equal(2, p.Rolls.Count); Check.Equal(5, p.Materials.Single(x => x.Key == "phosphorus").Required);
var already = Plan(Module(new() { Blueprint = "Weapon_Overcharged", Level = 5, Quality = 1, ExperimentalEffectId = "special_incendiary_rounds" }), Tod(5, 100), effect: effect);
Check.Equal(0, already.Materials.Count);
}
[Test] public void RemoteCraftNeverRaisesReputationAndCannotApplyExperimental()
{
var p = Plan(Module(), Tod(4, 99), 4, remote: true);
Check.Equal(4, p.FinalRank); Check.Equal(99d, p.FinalReputation!.Value); Check.That(p.Rolls.All(r => r.BeforeRank == r.AfterRank));
var effect = Catalog.Recipes.First(r => r.Type == "Multi-cannon" && r.Grade == 0);
Check.Throws<InvalidOperationException>(() => Plan(Module(), Tod(5, 100), effect: effect, remote: true));
}
[Test] public void ActualExistingEffectIsExplicitAndDoesNotRequireReapplyingItRemotely()
{
var effect=Catalog.Recipes.Single(r=>r.Type=="Multi-cannon"&&r.Name=="Incendiary Rounds"&&r.Grade==0);
var module=Module(new(){Blueprint="Weapon_Overcharged",Level=5,Quality=1,ExperimentalEffect="Incendiary Rounds"});
var current=Plan(module,Tod(5,100),effect:effect,remote:true);
Check.Equal(0,current.Materials.Count);
var none=EngineeringPlanner.Calculate(Catalog,module,"Overcharged Weapon",5,1,null,Tod(5,100),[],experimentalId:"");
Check.Equal(0,none.Materials.Count);Check.That(none.Notes.Any(n=>n=="目标实验效果:无实验效果。"));
Check.That(EngineeringExperimental.Same(Catalog,"Multi-cannon","special_incendiary_rounds","Incendiary Rounds"));
}
[Test] public void UnknownExistingEffectCanBeNamedWithoutInventingItsMaterialRecipe()
{
const string unknown="special_unlisted_effect";
var module=Module(new(){Blueprint="Weapon_Overcharged",Level=5,Quality=1,ExperimentalEffectId=unknown});
var unchanged=EngineeringPlanner.Calculate(Catalog,module,"Overcharged Weapon",5,1,null,Tod(5,100),[],remote:true,experimentalId:unknown);
Check.Equal(0,unchanged.Materials.Count);
Check.Throws<InvalidOperationException>(()=>EngineeringPlanner.Calculate(Catalog,Module(),"Overcharged Weapon",5,1,null,Tod(5,100),[],experimentalId:unknown));
}
[Test] public void ReputationIntervalProducesMaterialRanges()
{
var p = Plan(Module(), Tod(4, 21) with { ReputationMax = 99.99 });
Check.That(!p.Exact && p.Complete); Check.That(p.Materials.Any(m => m.Minimum < m.Required));
}
[Test] public void ModuleCapabilityAndLockedEngineerAreEnforced()
{
Check.Throws<InvalidOperationException>(() => Plan(Module(), Tod(5, 100) with { Progress = "Known" }));
Check.Throws<InvalidOperationException>(() => EngineeringPlanner.Calculate(Catalog, new() { Item = "int_hyperdrive_size5_class5" }, "Increased FSD Range", 5, 1, null, Tod(5, 100), []));
}
[Test] public void SnapshotAndCraftUpdateReputationInventoryAndModuleImmediately()
{
var p = new PlayerProjector();
p.Apply(Check.Json("""{"event":"LoadGame","timestamp":"2026-10-21T00:00:00Z","FID":"test","ShipID":1}"""));
p.Apply(Check.Json("""{"event":"Materials","timestamp":"2026-10-21T00:00:01Z","Raw":[{"Name":"nickel","Count":20}],"Encoded":[],"Manufactured":[]}"""));
p.Apply(Check.Json("""{"event":"Loadout","timestamp":"2026-10-21T00:00:02Z","ShipID":1,"Modules":[{"Slot":"LargeHardpoint1","Item":"hpt_multicannon_gimbal_large","Engineering":{"BlueprintName":"Weapon_Overcharged","Level":3,"Quality":0.2}}]}"""));
p.Apply(Check.Json("""{"event":"EngineerProgress","timestamp":"2026-10-21T00:00:03Z","Engineers":[{"Engineer":"Tod 'The Blaster' McQuinn","EngineerID":300260,"Progress":"Unlocked","Rank":4,"RankProgress":50}]}"""));
p.Apply(Check.Json("""{"event":"EngineerCraft","timestamp":"2026-10-21T00:00:04Z","EngineerID":300260,"Engineer":"Tod 'The Blaster' McQuinn","Slot":"LargeHardpoint1","Module":"hpt_multicannon_gimbal_large","BlueprintName":"Weapon_Overcharged","Level":3,"Quality":0.45,"Ingredients":[{"Name":"nickel","Count":1}]}"""));
Check.Equal(19, p.State.Materials.Single().Count); Check.Equal(.45, p.State.Ships.Single().Modules.Single().Engineering!.Quality!.Value);
Check.That(Math.Abs(p.State.Engineers.Single().ReputationMin!.Value - 65.9) < .00001); Check.Equal(1, p.State.VerifiedEngineeringRolls);
Check.That(!p.State.Ships.Single().ConfigurationChanged);
}
[Test] public void SparseRankUpdateKeepsUnlockAndDoesNotInventZeroProgress()
{
var p = new PlayerProjector();
p.Apply(Check.Json("""{"event":"EngineerProgress","timestamp":"2026-10-21T00:00:00Z","Engineers":[{"Engineer":"Example","EngineerID":1,"Progress":"Unlocked","Rank":1,"RankProgress":0}]}"""));
p.Apply(Check.Json("""{"event":"EngineerProgress","timestamp":"2026-10-21T00:00:01Z","Engineer":"Example","EngineerID":1,"Rank":4}"""));
var e = p.State.Engineers.Single(); Check.That(e.Unlocked); Check.Equal(4, e.Rank); Check.That(e.RankProgress is null && e.ReputationMax > e.ReputationMin);
}
[Test] public void ModuleProgressJustBelowThresholdCannotSkipPaidRoll()
{
var p = Plan(Module(new() { Blueprint = "Weapon_Overcharged", Level = 3, Quality = .799 }), Tod(5, 100), 4);
Check.That(p.Rolls.Any(r => r.Grade == 3));
}
[Test] public void ExperimentalAndRemoteJournalCraftDoNotIncreaseReputation()
{
var p = new PlayerProjector();
p.Apply(Check.Json("""{"event":"EngineerProgress","timestamp":"2026-10-21T00:00:00Z","Engineers":[{"Engineer":"Example","EngineerID":1,"Progress":"Unlocked","Rank":4,"RankProgress":0}]}"""));
p.Apply(Check.Json("""{"event":"EngineerCraft","timestamp":"2026-10-21T00:00:01Z","EngineerID":1,"Level":4,"ApplyExperimentalEffect":"special_auto_loader","Ingredients":[]}"""));
p.Apply(Check.Json("""{"event":"EngineerCraft","timestamp":"2026-10-21T00:00:02Z","EngineerID":1,"Level":4,"IsRemote":true,"Ingredients":[]}"""));
Check.Equal(21d, p.State.Engineers.Single().ReputationMin!.Value);
}
[Test] public void MissionRewardAndMaterialDonationMaintainInventory()
{
var p = new PlayerProjector();
p.Apply(Check.Json("""{"event":"Materials","timestamp":"2026-10-21T00:00:00Z","Raw":[{"Name":"iron","Count":10}],"Encoded":[],"Manufactured":[]}"""));
p.Apply(Check.Json("""{"event":"MissionCompleted","timestamp":"2026-10-21T00:00:01Z","MaterialsReward":[{"Name":"iron","Category":"Raw","Count":4}]}"""));
p.Apply(Check.Json("""{"event":"EngineerContribution","timestamp":"2026-10-21T00:00:02Z","Type":"materials","Material":"iron","Quantity":5}"""));
Check.Equal(9, p.State.Materials.Single().Count); Check.That(p.State.MaterialsReliable);
}
[Test] public void WorkshopTradeInvalidatesPreciseReputationWithoutFalselyRejectingLaterRoll()
{
var p = new EngineerProjector();
var workshop = Catalog.EngineerMarkets[300260];
p.Apply(Check.Json("""{"event":"EngineerProgress","timestamp":"2026-10-21T00:00:01Z","Engineers":[{"Engineer":"Tod 'The Blaster' McQuinn","EngineerID":300260,"Progress":"Unlocked","Rank":4,"RankProgress":0}]}"""), [], 1);
p.Apply(Check.Json("""{"event":"MarketSell","timestamp":"2026-10-21T00:00:02Z"}"""), [], 1, workshop);
Check.Equal(DateTimeOffset.Parse("2026-10-21T00:00:02Z"),p.UpdatedAt!.Value);
Check.Equal(100d,p.Engineers.Single().ReputationMax!.Value);
var module = Module(new(){Blueprint="Weapon_Overcharged",Level=3,Quality=.2});
p.Apply(Check.Json("""{"event":"EngineerCraft","timestamp":"2026-10-21T00:00:03Z","EngineerID":300260,"Slot":"LargeHardpoint1","Module":"hpt_multicannon_gimbal_large","BlueprintName":"Weapon_Overcharged","Level":3,"Quality":0.5333333333}"""),[new(){ShipId=1,Modules=[module]}],1);
Check.Equal(1,p.VerifiedQualityRolls);Check.Equal(0,p.MismatchedQualityRolls);
}
}
using XFEExtension.NetCore.XUnit.Attributes;
using XFEEDToolBox.Core.Data;
using XFEEDToolBox.Core.Engineering;
using XFEEDToolBox.Core.Models;
namespace XFEEDToolBox.Test;
[TestFixture]
public class EngineeringTests
{
private static readonly EngineeringCatalog Catalog = EngineeringCatalog.Default;
private static FittedModule Module(ModuleEngineering? engineering = null) => new() { Slot = "LargeHardpoint1", Item = "hpt_multicannon_gimbal_large", Engineering = engineering };
private static EngineerAccess Tod(int rank, double reputation) => new() { Id = 300260, Name = "Tod 'The Blaster' McQuinn", Rank = rank, Progress = "Unlocked", ReputationMin = reputation, ReputationMax = reputation };
private static EngineeringPlan Plan(FittedModule m, EngineerAccess e, int grade = 5, double quality = 1, EngineeringRecipe? effect = null, bool remote = false) => EngineeringPlanner.Calculate(Catalog, m, "Overcharged Weapon", grade, quality, effect, e, [], remote);
[Test] public void CapturedRulesPreserveOverflowAndValidateThresholds()
{
var r = EngineeringRules.Captured; Check.Equal(4, r.Rank(99.99)); Check.Equal(5, r.Rank(100)); Check.Equal(5.4, r.Reward(3)); Check.Equal(80d, r.ProgressPercent);
Check.Equal(60.5, r.Absolute(4, 50));
}
[Test] public void RankFourPromotesWithinGradeAndReducesRemainingRolls()
{
var m = Module(new() { Blueprint = "Weapon_Overcharged", Level = 3, Quality = .2 });
var p = Plan(m, Tod(4, 94.6), 3);
Check.Equal(3, p.Rolls.Count); Check.Equal(4, p.Rolls[0].BeforeRank); Check.Equal(5, p.Rolls[0].AfterRank);
Check.Equal(5, p.Rolls[1].BeforeRank); Check.That(p.Rolls[1].AfterQuality - p.Rolls[1].BeforeQuality > .33);
Check.That(p.Complete && p.Exact); Check.Equal(3, p.Materials.Single(x => x.Key == "nickel").Required);
}
[Test] public void FreshEngineerLevelsDynamicallyToFiveDuringOneModule()
{
var p = Plan(Module(), Tod(1, 0)); Check.That(p.Complete && p.FinalRank == 5);
Check.Equal(4, p.Rolls.Count(x => x.Grade == 1)); Check.Equal(5, p.Rolls.Count(x => x.Grade == 5));
Check.That(p.Rolls.Any(x => x.BeforeRank == 4 && x.AfterRank == 5));
Check.That(p.Rolls.Any(x => x.BeforeReputation < 1 && x.AfterReputation > 1));
}
[Test] public void MaxRankUsesOneTwoThreeFourFiveRolls()
{
var p = Plan(Module(), Tod(5, 100)); Check.Equal(15, p.Rolls.Count);
for (int grade = 1; grade <= 5; grade++) Check.Equal(grade, p.Rolls.Count(x => x.Grade == grade));
}
[Test] public void PartialGradeCountsOnlyRemainingProgressAndExperimentalOnce()
{
var effect = Catalog.Recipes.Single(r => r.Type == "Multi-cannon" && r.Name == "Incendiary Rounds" && r.Grade == 0);
var p = Plan(Module(new() { Blueprint = "Weapon_Overcharged", Level = 5, Quality = .6 }), Tod(5, 100), effect: effect);
Check.Equal(2, p.Rolls.Count); Check.Equal(5, p.Materials.Single(x => x.Key == "phosphorus").Required);
var already = Plan(Module(new() { Blueprint = "Weapon_Overcharged", Level = 5, Quality = 1, ExperimentalEffectId = "special_incendiary_rounds" }), Tod(5, 100), effect: effect);
Check.Equal(0, already.Materials.Count);
}
[Test] public void RemoteCraftNeverRaisesReputationAndCannotApplyExperimental()
{
var p = Plan(Module(), Tod(4, 99), 4, remote: true);
Check.Equal(4, p.FinalRank); Check.Equal(99d, p.FinalReputation!.Value); Check.That(p.Rolls.All(r => r.BeforeRank == r.AfterRank));
var effect = Catalog.Recipes.First(r => r.Type == "Multi-cannon" && r.Grade == 0);
Check.Throws<InvalidOperationException>(() => Plan(Module(), Tod(5, 100), effect: effect, remote: true));
}
[Test] public void ActualExistingEffectIsExplicitAndDoesNotRequireReapplyingItRemotely()
{
var effect=Catalog.Recipes.Single(r=>r.Type=="Multi-cannon"&&r.Name=="Incendiary Rounds"&&r.Grade==0);
var module=Module(new(){Blueprint="Weapon_Overcharged",Level=5,Quality=1,ExperimentalEffect="Incendiary Rounds"});
var current=Plan(module,Tod(5,100),effect:effect,remote:true);
Check.Equal(0,current.Materials.Count);
var none=EngineeringPlanner.Calculate(Catalog,module,"Overcharged Weapon",5,1,null,Tod(5,100),[],experimentalId:"");
Check.Equal(0,none.Materials.Count);Check.That(none.Notes.Any(n=>n=="目标实验效果:无实验效果。"));
Check.That(EngineeringExperimental.Same(Catalog,"Multi-cannon","special_incendiary_rounds","Incendiary Rounds"));
}
[Test] public void UnknownExistingEffectCanBeNamedWithoutInventingItsMaterialRecipe()
{
const string unknown="special_unlisted_effect";
var module=Module(new(){Blueprint="Weapon_Overcharged",Level=5,Quality=1,ExperimentalEffectId=unknown});
var unchanged=EngineeringPlanner.Calculate(Catalog,module,"Overcharged Weapon",5,1,null,Tod(5,100),[],remote:true,experimentalId:unknown);
Check.Equal(0,unchanged.Materials.Count);
Check.Throws<InvalidOperationException>(()=>EngineeringPlanner.Calculate(Catalog,Module(),"Overcharged Weapon",5,1,null,Tod(5,100),[],experimentalId:unknown));
}
[Test] public void ReputationIntervalProducesMaterialRanges()
{
var p = Plan(Module(), Tod(4, 21) with { ReputationMax = 99.99 });
Check.That(!p.Exact && p.Complete); Check.That(p.Materials.Any(m => m.Minimum < m.Required));
}
[Test] public void ModuleCapabilityAndLockedEngineerAreEnforced()
{
Check.Throws<InvalidOperationException>(() => Plan(Module(), Tod(5, 100) with { Progress = "Known" }));
Check.Throws<InvalidOperationException>(() => EngineeringPlanner.Calculate(Catalog, new() { Item = "int_hyperdrive_size5_class5" }, "Increased FSD Range", 5, 1, null, Tod(5, 100), []));
}
[Test] public void SnapshotAndCraftUpdateReputationInventoryAndModuleImmediately()
{
var p = new PlayerProjector();
p.Apply(Check.Json("""{"event":"LoadGame","timestamp":"2026-10-21T00:00:00Z","FID":"test","ShipID":1}"""));
p.Apply(Check.Json("""{"event":"Materials","timestamp":"2026-10-21T00:00:01Z","Raw":[{"Name":"nickel","Count":20}],"Encoded":[],"Manufactured":[]}"""));
p.Apply(Check.Json("""{"event":"Loadout","timestamp":"2026-10-21T00:00:02Z","ShipID":1,"Modules":[{"Slot":"LargeHardpoint1","Item":"hpt_multicannon_gimbal_large","Engineering":{"BlueprintName":"Weapon_Overcharged","Level":3,"Quality":0.2}}]}"""));
p.Apply(Check.Json("""{"event":"EngineerProgress","timestamp":"2026-10-21T00:00:03Z","Engineers":[{"Engineer":"Tod 'The Blaster' McQuinn","EngineerID":300260,"Progress":"Unlocked","Rank":4,"RankProgress":50}]}"""));
p.Apply(Check.Json("""{"event":"EngineerCraft","timestamp":"2026-10-21T00:00:04Z","EngineerID":300260,"Engineer":"Tod 'The Blaster' McQuinn","Slot":"LargeHardpoint1","Module":"hpt_multicannon_gimbal_large","BlueprintName":"Weapon_Overcharged","Level":3,"Quality":0.45,"Ingredients":[{"Name":"nickel","Count":1}]}"""));
Check.Equal(19, p.State.Materials.Single().Count); Check.Equal(.45, p.State.Ships.Single().Modules.Single().Engineering!.Quality!.Value);
Check.That(Math.Abs(p.State.Engineers.Single().ReputationMin!.Value - 65.9) < .00001); Check.Equal(1, p.State.VerifiedEngineeringRolls);
Check.That(!p.State.Ships.Single().ConfigurationChanged);
}
[Test] public void SparseRankUpdateKeepsUnlockAndDoesNotInventZeroProgress()
{
var p = new PlayerProjector();
p.Apply(Check.Json("""{"event":"EngineerProgress","timestamp":"2026-10-21T00:00:00Z","Engineers":[{"Engineer":"Example","EngineerID":1,"Progress":"Unlocked","Rank":1,"RankProgress":0}]}"""));
p.Apply(Check.Json("""{"event":"EngineerProgress","timestamp":"2026-10-21T00:00:01Z","Engineer":"Example","EngineerID":1,"Rank":4}"""));
var e = p.State.Engineers.Single(); Check.That(e.Unlocked); Check.Equal(4, e.Rank); Check.That(e.RankProgress is null && e.ReputationMax > e.ReputationMin);
}
[Test] public void ModuleProgressJustBelowThresholdCannotSkipPaidRoll()
{
var p = Plan(Module(new() { Blueprint = "Weapon_Overcharged", Level = 3, Quality = .799 }), Tod(5, 100), 4);
Check.That(p.Rolls.Any(r => r.Grade == 3));
}
[Test] public void ExperimentalAndRemoteJournalCraftDoNotIncreaseReputation()
{
var p = new PlayerProjector();
p.Apply(Check.Json("""{"event":"EngineerProgress","timestamp":"2026-10-21T00:00:00Z","Engineers":[{"Engineer":"Example","EngineerID":1,"Progress":"Unlocked","Rank":4,"RankProgress":0}]}"""));
p.Apply(Check.Json("""{"event":"EngineerCraft","timestamp":"2026-10-21T00:00:01Z","EngineerID":1,"Level":4,"ApplyExperimentalEffect":"special_auto_loader","Ingredients":[]}"""));
p.Apply(Check.Json("""{"event":"EngineerCraft","timestamp":"2026-10-21T00:00:02Z","EngineerID":1,"Level":4,"IsRemote":true,"Ingredients":[]}"""));
Check.Equal(21d, p.State.Engineers.Single().ReputationMin!.Value);
}
[Test] public void MissionRewardAndMaterialDonationMaintainInventory()
{
var p = new PlayerProjector();
p.Apply(Check.Json("""{"event":"Materials","timestamp":"2026-10-21T00:00:00Z","Raw":[{"Name":"iron","Count":10}],"Encoded":[],"Manufactured":[]}"""));
p.Apply(Check.Json("""{"event":"MissionCompleted","timestamp":"2026-10-21T00:00:01Z","MaterialsReward":[{"Name":"iron","Category":"Raw","Count":4}]}"""));
p.Apply(Check.Json("""{"event":"EngineerContribution","timestamp":"2026-10-21T00:00:02Z","Type":"materials","Material":"iron","Quantity":5}"""));
Check.Equal(9, p.State.Materials.Single().Count); Check.That(p.State.MaterialsReliable);
}
[Test] public void WorkshopTradeInvalidatesPreciseReputationWithoutFalselyRejectingLaterRoll()
{
var p = new EngineerProjector();
var workshop = Catalog.EngineerMarkets[300260];
p.Apply(Check.Json("""{"event":"EngineerProgress","timestamp":"2026-10-21T00:00:01Z","Engineers":[{"Engineer":"Tod 'The Blaster' McQuinn","EngineerID":300260,"Progress":"Unlocked","Rank":4,"RankProgress":0}]}"""), [], 1);
p.Apply(Check.Json("""{"event":"MarketSell","timestamp":"2026-10-21T00:00:02Z"}"""), [], 1, workshop);
Check.Equal(DateTimeOffset.Parse("2026-10-21T00:00:02Z"),p.UpdatedAt!.Value);
Check.Equal(100d,p.Engineers.Single().ReputationMax!.Value);
var module = Module(new(){Blueprint="Weapon_Overcharged",Level=3,Quality=.2});
p.Apply(Check.Json("""{"event":"EngineerCraft","timestamp":"2026-10-21T00:00:03Z","EngineerID":300260,"Slot":"LargeHardpoint1","Module":"hpt_multicannon_gimbal_large","BlueprintName":"Weapon_Overcharged","Level":3,"Quality":0.5333333333}"""),[new(){ShipId=1,Modules=[module]}],1);
Check.Equal(1,p.VerifiedQualityRolls);Check.Equal(0,p.MismatchedQualityRolls);
}
}