From d98b8ba56b8d92346404f992cd90dfb2e9400c31 Mon Sep 17 00:00:00 2001 From: Philippe Matray Date: Thu, 13 Aug 2026 15:28:38 +0200 Subject: [PATCH] test(mudblazor): allowlist the record-item model the shape guard flags --- .../Fields/CollectionItemShapeGuardTests.cs | 59 ++++++++++++++++++- .../Fields/FieldConfigurationRefreshTests.cs | 18 +++++- 2 files changed, 74 insertions(+), 3 deletions(-) diff --git a/FormCraft.ForMudBlazor.UnitTests/Fields/CollectionItemShapeGuardTests.cs b/FormCraft.ForMudBlazor.UnitTests/Fields/CollectionItemShapeGuardTests.cs index b837b8e..107da36 100644 --- a/FormCraft.ForMudBlazor.UnitTests/Fields/CollectionItemShapeGuardTests.cs +++ b/FormCraft.ForMudBlazor.UnitTests/Fields/CollectionItemShapeGuardTests.cs @@ -23,12 +23,44 @@ namespace FormCraft.ForMudBlazor.UnitTests.Fields; /// public class CollectionItemShapeGuardTests { + /// + /// Collection roots that re-declare a fixture shape deliberately, each with the reason the + /// fixture cannot serve. + /// + /// + /// + /// ⚠️ Every entry here is a hole in the guard, so an entry has to say what the fixture + /// cannot express — not that a local copy was quicker. "Convenience" is the case the guard + /// exists to catch. + /// + /// + /// keeps + /// this list honest: an entry whose suite has since migrated to the fixture stops suppressing + /// anything, and a stale entry would silently absolve a future copy of that same type. + /// + /// + private static readonly IReadOnlySet DeliberateLocalCopies = new HashSet + { + // FieldConfigurationRefreshTests.EqualityItemModel (#298) — holds a `record` item, and the + // record IS the test: that suite reproduces the duplicate-key render error a keyed collection + // loop raises for two rows comparing EQUAL BY VALUE. Every CollectionItemFixture model is a + // plain class with reference equality, so adopting the fixture would make the test vacuous — + // it would stop reproducing the error at all. + // + // The guard compares member types, which here are identical ("String"). The salient + // difference is the type's equality semantics, which a shape signature deliberately does not + // encode — widening the signature to include it would make the guard miss the real copies it + // was built for (#258's CredentialsModel/VaultModel), so the escape hatch is the right tool. + typeof(FieldConfigurationRefreshTests.EqualityItemModel), + }; + [Fact] public void No_Suite_Should_Re_Declare_A_Collection_Item_Shape_The_Fixture_Provides() { // Arrange & Act - the guard as CI runs it, over the whole assembly. var offenders = CollectionItemShapeGuard.FindOffenders( - CollectionItemShapeGuard.TestAssemblyTypes().Where(t => t.DeclaringType != typeof(Offending))); + CollectionItemShapeGuard.TestAssemblyTypes().Where(t => t.DeclaringType != typeof(Offending)), + DeliberateLocalCopies); // Assert - the message has to name the offender AND say what to do, because the reader is a // contributor who has just watched a green build turn red on a file they did not touch. @@ -39,6 +71,31 @@ public void No_Suite_Should_Re_Declare_A_Collection_Item_Shape_The_Fixture_Provi + string.Join("\n ", offenders.Select(o => o.Detail))); } + [Fact] + public void Every_Allowlisted_Type_Should_Still_Be_An_Offender_Without_The_Allowlist() + { + // Arrange - the allowlist's own guard. An entry stops suppressing anything the day its suite + // adopts the fixture, and a stale entry is worse than none: it silently absolves a FUTURE + // re-declaration of that same type. This is the same failure the file's header describes for + // the detection path — a check that has quietly stopped checking still reports green. + var universe = CollectionItemShapeGuard.TestAssemblyTypes() + .Where(t => t.DeclaringType != typeof(Offending)) + .ToList(); + + // Act - the identical run, minus the allowlist. + var withoutAllowlist = CollectionItemShapeGuard.FindOffenders(universe); + + // Assert - each entry must be carrying its weight, and be named if it is not. + foreach (var allowed in DeliberateLocalCopies) + { + withoutAllowlist.ShouldContain( + o => o.Owner == allowed, + $"{allowed.Name} is on the deliberate-local-copies allowlist but is no longer an " + + "offender. Either its suite adopted CollectionItemFixture — in which case delete " + + "the entry — or the guard stopped detecting it, which is the more serious reading."); + } + } + [Fact] public void The_Guard_Should_Flag_A_Nested_Copy_Of_A_Fixture_Item_Shape() { diff --git a/FormCraft.ForMudBlazor.UnitTests/Fields/FieldConfigurationRefreshTests.cs b/FormCraft.ForMudBlazor.UnitTests/Fields/FieldConfigurationRefreshTests.cs index c6afbf9..94d7e6f 100644 --- a/FormCraft.ForMudBlazor.UnitTests/Fields/FieldConfigurationRefreshTests.cs +++ b/FormCraft.ForMudBlazor.UnitTests/Fields/FieldConfigurationRefreshTests.cs @@ -274,12 +274,26 @@ public void Rows_Whose_Items_Compare_Equal_Should_Render_Without_A_Duplicate_Key component.FindAll("input").Count.ShouldBe(3); } - private sealed record EqualityItem + /// + /// A value-equality row. The record is the point — see + /// . + /// + /// + /// ⛔ Do not "share this with the fixture". Every CollectionItemFixture model is a plain + /// class with reference equality, so swapping one in makes that test vacuous — it would stop + /// reproducing the duplicate-key error entirely. internal rather than private only + /// so CollectionItemShapeGuardTests can name it in its deliberate-copies allowlist (#297). + /// + internal sealed record EqualityItem { public string ProductName { get; set; } = string.Empty; } - private sealed class EqualityItemModel + /// + /// The collection root for . Allowlisted in + /// CollectionItemShapeGuardTests — see the reason recorded there. + /// + internal sealed class EqualityItemModel { public List Items { get; set; } = []; }