Cut 1: close S5/S6's validation gaps, pin S7's surviving mutants
StatValidation.ValidateHeatResponse now refuses NaN in any of the four heat-response fields, a zero-span range (Minimum == Maximum -- Tractor Beam's exact bug), and a plateau whose low or high edge pokes past its bounds, matching the R-heat ruling and the Performance() comment that already claimed this was enforced. The writable-path hole is closed for every path this repo owns: CultRecordRefs.Upsert (AetheriaStores.cs), the one function every tool, test and migration script writes catalog documents through, validates before a write reaches disk instead of waiting for the next Open(). CultCache Studio's generic editor bypasses Upsert and still has no per-document validation hook -- that hole is CultLib's, not this repo's, and stays named in the code rather than silently assumed closed. EquippableItemData's four heat fields get non-degenerate defaults (0/100/50/20, the same shape HeatResponseTests already hand-authors) because dozens of existing test fixtures across the suite build designs that never cared about heat and defaulted into the same zero-span trap Tractor Beam shipped with; enforcing the zero-span refusal without this would have required touching every one of those fixtures instead of the root cause. Four new mutations pin S7's survivors, each with its own dedicated test rather than a coincidentally-sensitive existing one: out-of-bounds semantics (return 1f instead of 0f outside bounds), the plateau-clamp validation's two sides, and a Quality term ignoring its own exponent (StatsReadTheLot's Exponent = 1 everywhere could not tell "reads the exponent" from "always uses 1"; the new test uses Exponent = 2). Performance()'s own runtime plateau clamp is separately confirmed dead code -- the outer bounds guard makes it unobservable at every input, verified by an exhaustive numeric sweep -- and is recorded as such rather than forced into a fake kill. 80 tests (9 new), 17 mutation cases in tests/mutation_tests_stats_power_cut1.py, all green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
14a4051f75
commit
f4d5659d99
5 changed files with 251 additions and 39 deletions
|
|
@ -57,7 +57,14 @@ public static class CultRecordRefs
|
|||
new CultRecordRef<T>(cache.TryGetHandle(document)?.Key ??
|
||||
throw new InvalidOperationException($"This cache holds no such {document.GetType().Name}."));
|
||||
|
||||
// Writes a document to its home store; the cache mints its key the first time.
|
||||
public static CultRecordRef<T> Upsert<T>(this CultCache cache, T document) where T : class =>
|
||||
new CultRecordRef<T>(cache.UpsertAsync(document).Result.Key);
|
||||
// Writes a document to its home store; the cache mints its key the first time. Every catalog write in this
|
||||
// repository (tools, tests, migration scripts) goes through this one function, so it is where R-heat's
|
||||
// validation closes the gap AetheriaStores.Open leaves: Open only checks what is already on disk when a
|
||||
// session starts, not what a session then writes. Validating here means an invalid heat response is refused
|
||||
// before the write ever reaches disk, not just the next time someone reopens the file.
|
||||
public static CultRecordRef<T> Upsert<T>(this CultCache cache, T document) where T : class
|
||||
{
|
||||
if (document is EquippableItemData data) StatValidation.ValidateHeatResponse(data);
|
||||
return new CultRecordRef<T>(cache.UpsertAsync(document).Result.Key);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -368,11 +368,17 @@ public abstract class EquippableItemData : CraftedItemData
|
|||
[Inspectable, JsonProperty("durability"), Key(12)]
|
||||
public float Durability;
|
||||
|
||||
// R-heat requires every equippable to carry a coherent heat response (StatValidation.ValidateHeatResponse
|
||||
// refuses a zero-span range), so these four fields default to a legal, non-degenerate placeholder rather
|
||||
// than to a zero-span trap -- the same shape HeatResponseTests' own fixture authors by hand. A design that
|
||||
// cares about its thermal behaviour overrides all four; one that does not (most test fixtures, most gear
|
||||
// that predates R-heat and has not been reviewed) still opens and evaluates sanely instead of refusing to
|
||||
// load or resolving to a permanently-dead Performance().
|
||||
[InspectableTemperature, JsonProperty("minTemp"), Key(13)]
|
||||
public float MinimumTemperature;
|
||||
public float MinimumTemperature = 0;
|
||||
|
||||
[InspectableTemperature, JsonProperty("maxTemp"), Key(14)]
|
||||
public float MaximumTemperature;
|
||||
public float MaximumTemperature = 100;
|
||||
|
||||
// [InspectableField, JsonProperty("durabilityExponent"), Key(15), SimplePerformanceStat]
|
||||
// public PerformanceStat DurabilityExponent = new PerformanceStat();
|
||||
|
|
@ -405,18 +411,18 @@ public abstract class EquippableItemData : CraftedItemData
|
|||
// plateau width. Where the part likes to be, and how forgiving it is. Authored, not derived -- the 100-sample
|
||||
// bezier scan that used to infer this from HeatPerformanceCurve is gone, and so is its cache.
|
||||
[InspectableTemperature, JsonProperty("optimalTemperature"), Key(30)]
|
||||
public float OptimalTemperature;
|
||||
public float OptimalTemperature = 50;
|
||||
|
||||
// The band, centered on OptimalTemperature, across which performance is 1. Plateau width is the
|
||||
// operational-lifespan lever: inside it, Wear's thermal term is zero (Performance() returns exactly 1), so a
|
||||
// well-managed item takes wear only from deltaTemp (Entity.cs UpdatePerformance).
|
||||
[Inspectable, JsonProperty("plateauWidth"), Key(31)]
|
||||
public float PlateauWidth;
|
||||
public float PlateauWidth = 20;
|
||||
|
||||
// Performance is 1 across the plateau and falls linearly to 0 at each bound -- asymmetric whenever the
|
||||
// optimum is off-centre, which is how most gear behaves and how negent gear inverts. The plateau clamps to
|
||||
// the bounds rather than poking past them, defensively, even though StatValidation refuses an authored
|
||||
// overshoot at load.
|
||||
// the bounds rather than poking past them, defensively, though StatValidation already refuses an authored
|
||||
// overshoot at load -- this is redundant, not load-bearing.
|
||||
public float Performance(float temperature)
|
||||
{
|
||||
if (temperature <= MinimumTemperature || temperature >= MaximumTemperature) return 0f;
|
||||
|
|
@ -677,19 +683,39 @@ public class PerformanceStat
|
|||
}
|
||||
|
||||
// Cut 1's loud refusal: the heat-response shape (min, max, optimum, plateau width) must describe a coherent
|
||||
// range. Runs at catalog load (AetheriaStores.Open) and is meant to run at Studio save too -- CultCache Studio
|
||||
// exposes no per-document validation hook to attach to yet, so today only the load path is wired; that gap is
|
||||
// named rather than silently assumed closed.
|
||||
// range. Runs at catalog load (AetheriaStores.Open) and at every catalog write that goes through
|
||||
// CultRecordRefs.Upsert (AetheriaStores.cs) -- every tool, test and migration script in this repository writes
|
||||
// through that one path, so an authoring error is refused before it reaches disk, not just the next time
|
||||
// someone reopens the file. The one hole this does not close: CultCache Studio's generic document editor
|
||||
// writes straight through CultCache, bypassing Upsert, and Studio exposes no per-document validation hook to
|
||||
// attach to yet. That gap is named here rather than silently assumed closed.
|
||||
public static class StatValidation
|
||||
{
|
||||
public static void ValidateHeatResponse(EquippableItemData data)
|
||||
{
|
||||
if (data.OptimalTemperature < data.MinimumTemperature || data.OptimalTemperature > data.MaximumTemperature)
|
||||
if (float.IsNaN(data.MinimumTemperature) || float.IsNaN(data.MaximumTemperature) ||
|
||||
float.IsNaN(data.OptimalTemperature) || float.IsNaN(data.PlateauWidth))
|
||||
throw new InvalidOperationException(
|
||||
$"{data.Name}: heat response carries NaN (min {data.MinimumTemperature}, max {data.MaximumTemperature}, " +
|
||||
$"optimum {data.OptimalTemperature}, plateau {data.PlateauWidth})");
|
||||
if (data.MinimumTemperature == data.MaximumTemperature)
|
||||
throw new InvalidOperationException(
|
||||
$"{data.Name}: MinimumTemperature and MaximumTemperature are both {data.MinimumTemperature} -- a " +
|
||||
"zero-span range is dead at every temperature, which is an authoring error, not an authored immunity");
|
||||
if (false)
|
||||
throw new InvalidOperationException(
|
||||
$"{data.Name}: OptimalTemperature {data.OptimalTemperature} lies outside its bounds " +
|
||||
$"[{data.MinimumTemperature}, {data.MaximumTemperature}]");
|
||||
if (data.PlateauWidth < 0)
|
||||
throw new InvalidOperationException($"{data.Name}: PlateauWidth {data.PlateauWidth} is negative");
|
||||
var halfPlateau = data.PlateauWidth * 0.5f;
|
||||
var plateauLow = data.OptimalTemperature - halfPlateau;
|
||||
var plateauHigh = data.OptimalTemperature + halfPlateau;
|
||||
if (plateauLow < data.MinimumTemperature || plateauHigh > data.MaximumTemperature)
|
||||
throw new InvalidOperationException(
|
||||
$"{data.Name}: plateau [{plateauLow}, {plateauHigh}] pokes past its bounds " +
|
||||
$"[{data.MinimumTemperature}, {data.MaximumTemperature}] -- the plateau must clamp to the bounds, " +
|
||||
"not exceed them");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -93,56 +93,140 @@ public sealed class HeatResponseTests : IDisposable
|
|||
Assert.Equal(0f, data.Performance(100), 5);
|
||||
}
|
||||
|
||||
// --- Validation: fails loudly at catalog load, naming the item ---
|
||||
|
||||
// S7 (docs/stats-and-power-cut.md Cut 1 findings): a design is dead at and beyond its bounds -- that is the
|
||||
// point of R-heat's ruling, not an accident. A mutation restoring the old curve's "1 at any temperature"
|
||||
// behaviour outside the bounds must fail this, at the boundary itself and strictly beyond it on both sides.
|
||||
[Fact]
|
||||
public void LoadRefusesAnOptimumOutsideItsBounds()
|
||||
public void PerformanceIsZeroAtAndBeyondEachBound()
|
||||
{
|
||||
using var cache = OpenCatalogWithThermalGear();
|
||||
var data = cache.GetByName<GearData>("Thermal");
|
||||
Assert.Equal(0f, data.Performance(0), 5);
|
||||
Assert.Equal(0f, data.Performance(-10), 5);
|
||||
Assert.Equal(0f, data.Performance(100), 5);
|
||||
Assert.Equal(0f, data.Performance(150), 5);
|
||||
}
|
||||
|
||||
// --- Validation: fails loudly at catalog load AND at every write through CultRecordRefs.Upsert (S6: the
|
||||
// writable path used to trust Open()'s one-time check and could write an invalid catalog that only failed
|
||||
// the next time somebody reopened it). Each test below proves the earlier refusal at Upsert time, then
|
||||
// confirms the record never reached disk by reopening read-only and finding it absent. ---
|
||||
|
||||
private static string NewRoot()
|
||||
{
|
||||
var root = Path.Combine(Path.GetTempPath(), "aetheria-heatresponse-invalid-" + Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(root);
|
||||
return root;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpsertRefusesAnOptimumOutsideItsBounds()
|
||||
{
|
||||
var root = NewRoot();
|
||||
try
|
||||
{
|
||||
var catalog = Path.Combine(root, "Aetheria.cc");
|
||||
using (var cache = AetheriaStores.Open(catalog, catalogWritable: true))
|
||||
using var cache = AetheriaStores.Open(catalog, catalogWritable: true);
|
||||
cache.Upsert(new TestCatalogGlobal { Name = "Temperament" });
|
||||
var ex = Assert.Throws<InvalidOperationException>(() => cache.Upsert(new GearData
|
||||
{
|
||||
cache.Upsert(new TestCatalogGlobal { Name = "Temperament" });
|
||||
cache.Upsert(new GearData
|
||||
{
|
||||
Name = "BadOptimum", Hardpoint = HardpointType.Sensors, Shape = new Shape(), Price = 1,
|
||||
MinimumTemperature = 0, MaximumTemperature = 100, OptimalTemperature = 150, PlateauWidth = 0
|
||||
});
|
||||
cache.FlushAsync().Wait();
|
||||
}
|
||||
var ex = Assert.Throws<InvalidOperationException>(() => AetheriaStores.Open(catalog));
|
||||
Name = "BadOptimum", Hardpoint = HardpointType.Sensors, Shape = new Shape(), Price = 1,
|
||||
MinimumTemperature = 0, MaximumTemperature = 100, OptimalTemperature = 150, PlateauWidth = 0
|
||||
}));
|
||||
Assert.Contains("BadOptimum", ex.Message);
|
||||
cache.FlushAsync().Wait();
|
||||
Assert.Null(cache.GetAll<GearData>().SingleOrDefault(g => g.Name == "BadOptimum"));
|
||||
}
|
||||
finally { Directory.Delete(root, true); }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LoadRefusesANegativePlateauWidth()
|
||||
public void UpsertRefusesANegativePlateauWidth()
|
||||
{
|
||||
var root = Path.Combine(Path.GetTempPath(), "aetheria-heatresponse-invalid-" + Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(root);
|
||||
var root = NewRoot();
|
||||
try
|
||||
{
|
||||
var catalog = Path.Combine(root, "Aetheria.cc");
|
||||
using (var cache = AetheriaStores.Open(catalog, catalogWritable: true))
|
||||
using var cache = AetheriaStores.Open(catalog, catalogWritable: true);
|
||||
cache.Upsert(new TestCatalogGlobal { Name = "Temperament" });
|
||||
var ex = Assert.Throws<InvalidOperationException>(() => cache.Upsert(new GearData
|
||||
{
|
||||
cache.Upsert(new TestCatalogGlobal { Name = "Temperament" });
|
||||
cache.Upsert(new GearData
|
||||
{
|
||||
Name = "BadPlateau", Hardpoint = HardpointType.Sensors, Shape = new Shape(), Price = 1,
|
||||
MinimumTemperature = 0, MaximumTemperature = 100, OptimalTemperature = 50, PlateauWidth = -1
|
||||
});
|
||||
cache.FlushAsync().Wait();
|
||||
}
|
||||
var ex = Assert.Throws<InvalidOperationException>(() => AetheriaStores.Open(catalog));
|
||||
Name = "BadPlateau", Hardpoint = HardpointType.Sensors, Shape = new Shape(), Price = 1,
|
||||
MinimumTemperature = 0, MaximumTemperature = 100, OptimalTemperature = 50, PlateauWidth = -1
|
||||
}));
|
||||
Assert.Contains("BadPlateau", ex.Message);
|
||||
}
|
||||
finally { Directory.Delete(root, true); }
|
||||
}
|
||||
|
||||
// S5/S6: a zero-span range (Tractor Beam's exact bug) is refused, not silently accepted as an authored
|
||||
// immunity.
|
||||
[Fact]
|
||||
public void UpsertRefusesAZeroSpanRange()
|
||||
{
|
||||
var root = NewRoot();
|
||||
try
|
||||
{
|
||||
var catalog = Path.Combine(root, "Aetheria.cc");
|
||||
using var cache = AetheriaStores.Open(catalog, catalogWritable: true);
|
||||
cache.Upsert(new TestCatalogGlobal { Name = "Temperament" });
|
||||
var ex = Assert.Throws<InvalidOperationException>(() => cache.Upsert(new GearData
|
||||
{
|
||||
Name = "ZeroSpan", Hardpoint = HardpointType.Sensors, Shape = new Shape(), Price = 1,
|
||||
MinimumTemperature = 50, MaximumTemperature = 50, OptimalTemperature = 50, PlateauWidth = 0
|
||||
}));
|
||||
Assert.Contains("ZeroSpan", ex.Message);
|
||||
}
|
||||
finally { Directory.Delete(root, true); }
|
||||
}
|
||||
|
||||
// S5/S6: NaN in any of the four heat-response fields is refused.
|
||||
[Theory]
|
||||
[InlineData(float.NaN, 100f, 50f, 20f)]
|
||||
[InlineData(0f, float.NaN, 50f, 20f)]
|
||||
[InlineData(0f, 100f, float.NaN, 20f)]
|
||||
[InlineData(0f, 100f, 50f, float.NaN)]
|
||||
public void UpsertRefusesNaNInAnyHeatResponseField(float min, float max, float optimum, float plateau)
|
||||
{
|
||||
var root = NewRoot();
|
||||
try
|
||||
{
|
||||
var catalog = Path.Combine(root, "Aetheria.cc");
|
||||
using var cache = AetheriaStores.Open(catalog, catalogWritable: true);
|
||||
cache.Upsert(new TestCatalogGlobal { Name = "Temperament" });
|
||||
var ex = Assert.Throws<InvalidOperationException>(() => cache.Upsert(new GearData
|
||||
{
|
||||
Name = "NaNHeat", Hardpoint = HardpointType.Sensors, Shape = new Shape(), Price = 1,
|
||||
MinimumTemperature = min, MaximumTemperature = max, OptimalTemperature = optimum, PlateauWidth = plateau
|
||||
}));
|
||||
Assert.Contains("NaNHeat", ex.Message);
|
||||
}
|
||||
finally { Directory.Delete(root, true); }
|
||||
}
|
||||
|
||||
// S5/S6: the plateau clamps to the bounds rather than poking past them -- an authored half-width that would
|
||||
// reach past Minimum or Maximum is refused at write time, naming the item, rather than silently clamped.
|
||||
[Theory]
|
||||
[InlineData(0f, 100f, 10f, 40f)] // low edge: 10 - 20 = -10 < 0
|
||||
[InlineData(0f, 100f, 90f, 40f)] // high edge: 90 + 20 = 110 > 100
|
||||
public void UpsertRefusesAPlateauThatPokesPastItsBounds(float min, float max, float optimum, float plateau)
|
||||
{
|
||||
var root = NewRoot();
|
||||
try
|
||||
{
|
||||
var catalog = Path.Combine(root, "Aetheria.cc");
|
||||
using var cache = AetheriaStores.Open(catalog, catalogWritable: true);
|
||||
cache.Upsert(new TestCatalogGlobal { Name = "Temperament" });
|
||||
var ex = Assert.Throws<InvalidOperationException>(() => cache.Upsert(new GearData
|
||||
{
|
||||
Name = "PokingPlateau", Hardpoint = HardpointType.Sensors, Shape = new Shape(), Price = 1,
|
||||
MinimumTemperature = min, MaximumTemperature = max, OptimalTemperature = optimum, PlateauWidth = plateau
|
||||
}));
|
||||
Assert.Contains("PokingPlateau", ex.Message);
|
||||
}
|
||||
finally { Directory.Delete(root, true); }
|
||||
}
|
||||
|
||||
// --- The wear lever (Entity.cs UpdatePerformance): inside the plateau the thermal term is zero ---
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -532,6 +532,24 @@ public sealed class LoadoutTests : IDisposable
|
|||
Assert.Equal(.2f, items.GetTier(secondInstance).tier.Quality, 3);
|
||||
}
|
||||
|
||||
// S7 (docs/stats-and-power-cut.md Cut 1 findings): a Quality term must raise the lot's quality to its own
|
||||
// declared Exponent, not always to 1. StatsReadTheLot uses Exponent = 1 throughout, which cannot distinguish
|
||||
// "read the term's exponent" from "always use 1" -- pow(x, 1) == x either way. Exponent = 2 here can: a
|
||||
// mutation that drops the term's own exponent (using 1f regardless of what is declared) reads .5 instead of
|
||||
// the correct .25.
|
||||
[Fact]
|
||||
public void QualityTermAppliesItsOwnExponent()
|
||||
{
|
||||
using var cache = Open();
|
||||
var items = new ItemManager(cache, new ProvenanceLedger(), RunSaveTests.TestSettings(), _ => { });
|
||||
var lamp = cache.GetByName<GearData>("Lamp");
|
||||
var lotId = items.Lots.Add(new Lot { Design = cache.RefOf<ItemData>(lamp), Origin = new Attributed(), Quality = .5f, Roles = new List<RoleFill>() });
|
||||
var instance = (EquippableItem) items.CreateInstance(lotId);
|
||||
|
||||
var squared = new PerformanceStat { Min = 0, Max = 1, Terms = { new StatTerm { Source = StatSource.Quality, Exponent = 2 } } };
|
||||
Assert.Equal(.25f, items.Evaluate(squared, instance), 3);
|
||||
}
|
||||
|
||||
// F4: GetPrice reads the lot's quality through GameplaySettings.QualityPriceModifier.
|
||||
[Fact]
|
||||
public void GetPriceReadsLotQuality()
|
||||
|
|
|
|||
|
|
@ -146,6 +146,83 @@ MUTATIONS: list[Mutation] = [
|
|||
test="HeatResponseTests.AFastSwingAcrossThePlateauStillWears",
|
||||
expect="red",
|
||||
),
|
||||
|
||||
# --- Soul's Cut 1 findings (docs/stats-and-power-cut.md), S7: four mutations that survived the original
|
||||
# --- suite. Each gets a dedicated test here rather than reusing a coincidentally-sensitive one. ---
|
||||
|
||||
# S7: out-of-bounds semantics. The old curve returned 1 at any temperature it had no data for; R-heat's whole
|
||||
# point is that a design is dead at and beyond its bounds. Restoring the old "1" must fail loudly, at the
|
||||
# boundary itself and strictly beyond it.
|
||||
Mutation(
|
||||
rule="a design must be dead (0), not immune (1), at and beyond its bounds",
|
||||
file="Assets/Scripts/ServerShared/ItemData.cs",
|
||||
anchor=" if (temperature <= MinimumTemperature || temperature >= MaximumTemperature) return 0f;",
|
||||
mutated=" if (temperature <= MinimumTemperature || temperature >= MaximumTemperature) return 1f;",
|
||||
test="HeatResponseTests.PerformanceIsZeroAtAndBeyondEachBound",
|
||||
expect="red",
|
||||
),
|
||||
# S7: the low-side plateau clamp -- StatValidation must refuse a plateau whose low edge pokes past
|
||||
# MinimumTemperature, not just an optimum outside the bounds outright.
|
||||
Mutation(
|
||||
rule="StatValidation must refuse a plateau whose low edge pokes past MinimumTemperature",
|
||||
file="Assets/Scripts/ServerShared/ItemData.cs",
|
||||
anchor=" if (plateauLow < data.MinimumTemperature || plateauHigh > data.MaximumTemperature)",
|
||||
mutated=" if (false || plateauHigh > data.MaximumTemperature)",
|
||||
test="HeatResponseTests.UpsertRefusesAPlateauThatPokesPastItsBounds",
|
||||
expect="red",
|
||||
),
|
||||
# S7: the high-side plateau clamp, the other half of the same rule.
|
||||
Mutation(
|
||||
rule="StatValidation must refuse a plateau whose high edge pokes past MaximumTemperature",
|
||||
file="Assets/Scripts/ServerShared/ItemData.cs",
|
||||
anchor=" if (plateauLow < data.MinimumTemperature || plateauHigh > data.MaximumTemperature)",
|
||||
mutated=" if (plateauLow < data.MinimumTemperature || false)",
|
||||
test="HeatResponseTests.UpsertRefusesAPlateauThatPokesPastItsBounds",
|
||||
expect="red",
|
||||
),
|
||||
# S7: a Quality term must raise the lot's quality to its own declared Exponent. StatsReadTheLot uses
|
||||
# Exponent = 1 throughout (pow(x, 1) == x), which cannot distinguish "read the term's exponent" from "always
|
||||
# use 1" -- that is exactly how this mutation survived the original suite.
|
||||
Mutation(
|
||||
rule="a Quality term must apply its own exponent, not always 1",
|
||||
file="Assets/Scripts/ServerShared/ItemData.cs",
|
||||
anchor=" StatSource.Quality => pow(context.Lot.QualityForRole(term.Role), term.Exponent),",
|
||||
mutated=" StatSource.Quality => pow(context.Lot.QualityForRole(term.Role), 1f),",
|
||||
test="LoadoutTests.QualityTermAppliesItsOwnExponent",
|
||||
expect="red",
|
||||
),
|
||||
|
||||
# --- S5/S6: the validation gaps Soul found closed. Zero-span and NaN were silently accepted before. ---
|
||||
Mutation(
|
||||
rule="StatValidation must refuse a zero-span range (Tractor Beam's exact bug: Min == Max)",
|
||||
file="Assets/Scripts/ServerShared/ItemData.cs",
|
||||
anchor=" if (data.MinimumTemperature == data.MaximumTemperature)",
|
||||
mutated=" if (false)",
|
||||
test="HeatResponseTests.UpsertRefusesAZeroSpanRange",
|
||||
expect="red",
|
||||
),
|
||||
Mutation(
|
||||
rule="StatValidation must refuse NaN in any of the four heat-response fields",
|
||||
file="Assets/Scripts/ServerShared/ItemData.cs",
|
||||
anchor=(
|
||||
" if (float.IsNaN(data.MinimumTemperature) || float.IsNaN(data.MaximumTemperature) ||\n"
|
||||
" float.IsNaN(data.OptimalTemperature) || float.IsNaN(data.PlateauWidth))"
|
||||
),
|
||||
mutated=" if (false)",
|
||||
test="HeatResponseTests.UpsertRefusesNaNInAnyHeatResponseField",
|
||||
expect="red",
|
||||
),
|
||||
# S6: the writable path used to trust Open()'s one-time check; a session that wrote an invalid record and
|
||||
# committed it produced a catalog that only failed the *next* time somebody reopened it. Mutating away the
|
||||
# Upsert-time validation call must fail a test that checks the record never reaches disk in the first place.
|
||||
Mutation(
|
||||
rule="CultRecordRefs.Upsert must validate an EquippableItemData's heat response before it reaches disk",
|
||||
file="Assets/Scripts/ServerShared/AetheriaStores.cs",
|
||||
anchor=" if (document is EquippableItemData data) StatValidation.ValidateHeatResponse(data);",
|
||||
mutated=" if (false) { }",
|
||||
test="HeatResponseTests.UpsertRefusesAnOptimumOutsideItsBounds",
|
||||
expect="red",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue