Pin Brand's same-maker tie-break with a test and a mutant

Brand()'s ordering by record key was only exercised indirectly, through
a test that gives each candidate product a different maker. Add a test
with two FactionProductData for the same maker and same design, distinct
record keys, asserting Brand returns the one first in record-key order.

Add the corresponding mutant (OrderBy -> OrderByDescending at Brand's
ordering) to mutation_tests.py so a regression there gets caught.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-09-17 12:42:48 +02:00
commit 5e9634efcd
2 changed files with 33 additions and 0 deletions

View file

@ -316,6 +316,30 @@ public sealed class LoadoutTests : IDisposable
Assert.Contains("slot 0,0: no available product of Lamp", Assert.Single(failures));
}
// Census's duplicate (maker, design) detector exists precisely because this should never happen in an
// authored catalog; Brand()'s tie-break is what a runtime item falls back on if it ever does, so it must stay
// deterministic rather than however the cache happens to enumerate.
[Fact]
public void BrandPicksFirstProductInKeyOrderForSameMakerAndDesign()
{
using var cache = AetheriaStores.Open(Catalog, catalogWritable: true);
var maker = cache.Upsert(new Faction { Name = "TieBreaker", ShortName = "TIE" });
var lamp = new CultRecordRef<CraftedItemData>(cache.RefOf(cache.GetByName<GearData>("Lamp")).Key);
cache.Commit(batch => batch.Upsert(typeof(FactionProductData), new FactionProductData { Name = "lamp-b", Design = lamp, Manufacturer = maker }, new CultRecordKey("lamp-b")));
cache.Commit(batch => batch.Upsert(typeof(FactionProductData), new FactionProductData { Name = "lamp-a", Design = lamp, Manufacturer = maker }, new CultRecordKey("lamp-a")));
var items = new ItemManager(cache, new ProvenanceLedger(), RunSaveTests.TestSettings(), _ => { });
var lotId = items.Lots.Add(new Lot
{
Design = cache.RefOf<ItemData>(cache.GetByName<GearData>("Lamp")),
Origin = new Produced { Faction = maker, Station = 1, Facility = 2, Inputs = Array.Empty<int>() },
Quality = .5f, Roles = new List<RoleFill>()
});
var instance = (EquippableItem) items.CreateInstance(lotId);
var (_, product) = items.Brand(instance);
Assert.Equal("lamp-a", product.Name);
}
// F2: this test must not enumerate reloaded instances through EntitySerializer.Items, the GC root walk under
// test. It hand-mints one real lot per EntityPack root (Hull, Equipment, CargoBays, DockingBays, CargoContents,
// Children, DockingBayContents) so the expected lot ids are known independently of any enumerator, and reads

View file

@ -122,6 +122,15 @@ MUTATIONS: list[Mutation] = [
test="LoadoutTests.FirstAvailableProductInKeyOrderBuildsTheSlot",
expect="red",
),
# --- BrandPicksFirstProductInKeyOrderForSameMakerAndDesign: Brand's own tie-break must be ascending key order ---
Mutation(
rule="Brand's tie-break among a maker's products for one design is ascending record-key order",
file="Assets/Scripts/ServerShared/ItemManager.cs",
anchor=" .OrderBy(p => ItemData.RefOf(p).Key.Value, StringComparer.Ordinal)",
mutated=" .OrderByDescending(p => ItemData.RefOf(p).Key.Value, StringComparer.Ordinal)",
test="LoadoutTests.BrandPicksFirstProductInKeyOrderForSameMakerAndDesign",
expect="red",
),
# --- StatsReadTheLot: stats must read the lot, including its per-role fills ---
Mutation(
rule="Evaluate reads the lot's per-role fill for a role stat",