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; } = [];
}