From ede06b1bf62e4bbf71eb43e48c0feb6ee2a6f23f Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Mon, 6 Apr 2026 00:06:35 -0400 Subject: [PATCH] fix: enforce sentinel reactively in OnPlateAdded/OnPlateRemoved Without this, RemoveEmptyPlates would destroy the sentinel with no recovery, and tail-plate subscriptions would go stale after plate list mutations. Added tests for both scenarios. Co-Authored-By: Claude Opus 4.6 (1M context) --- OpenNest.Core/PlateManager.cs | 6 ++++ OpenNest.Tests/PlateManagerTests.cs | 45 ++++++++++++++++++++++++++++- 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/OpenNest.Core/PlateManager.cs b/OpenNest.Core/PlateManager.cs index 8708bc6..b5e1701 100644 --- a/OpenNest.Core/PlateManager.cs +++ b/OpenNest.Core/PlateManager.cs @@ -198,6 +198,9 @@ namespace OpenNest private void OnPlateAdded(object sender, ItemAddedEventArgs e) { + if (!suppressNavigation && !batching) + EnsureSentinel(); + PlateListChanged?.Invoke(this, EventArgs.Empty); if (!suppressNavigation) @@ -212,6 +215,9 @@ namespace OpenNest if (CurrentIndex >= Count && Count > 0) CurrentIndex = Count - 1; + if (!suppressNavigation && !batching) + EnsureSentinel(); + PlateListChanged?.Invoke(this, EventArgs.Empty); if (!suppressNavigation) diff --git a/OpenNest.Tests/PlateManagerTests.cs b/OpenNest.Tests/PlateManagerTests.cs index 9810528..a5bff2a 100644 --- a/OpenNest.Tests/PlateManagerTests.cs +++ b/OpenNest.Tests/PlateManagerTests.cs @@ -1,4 +1,5 @@ using OpenNest.CNC; +using OpenNest.Collections; using OpenNest.Geometry; namespace OpenNest.Tests; @@ -373,7 +374,10 @@ public class PlateManagerTests mgr.RemoveCurrent(); - Assert.Equal(1, nest.Plates.Count); + // plate1 + sentinel (created by reactive sentinel enforcement) + Assert.Equal(2, nest.Plates.Count); + Assert.Same(plate1, nest.Plates[0]); + Assert.Equal(0, nest.Plates[^1].Parts.Count); Assert.Equal(0, mgr.CurrentIndex); } @@ -394,4 +398,43 @@ public class PlateManagerTests mgr.LoadFirst(); Assert.True(mgr.CanRemoveCurrent); } + + [Fact] + public void RemoveEmptyPlates_SentinelIsRestored() + { + var nest = CreateNest(); + var plate = nest.CreatePlate(); + plate.Parts.Add(MakePart()); + using var mgr = new PlateManager(nest); + mgr.EnsureSentinel(); + Assert.Equal(2, nest.Plates.Count); + + // RemoveEmptyPlates removes the sentinel + nest.Plates.RemoveEmptyPlates(); + + // Sentinel should be restored reactively by OnPlateRemoved + Assert.Equal(2, nest.Plates.Count); + Assert.Equal(0, nest.Plates[^1].Parts.Count); + } + + [Fact] + public void PlateRemoval_RefreshesTailSubscriptions() + { + var nest = CreateNest(); + var plate1 = nest.CreatePlate(); + plate1.Parts.Add(MakePart()); + var plate2 = nest.CreatePlate(); + plate2.Parts.Add(MakePart()); + using var mgr = new PlateManager(nest); + mgr.EnsureSentinel(); + // plates: [plate1(parts), plate2(parts), sentinel(empty)] + + // Remove plate2 — tail subscriptions should refresh + nest.Plates.Remove(plate2); + // plates: [plate1(parts), sentinel(empty)] + + // Verify reactive subscriptions still work on the new tail + nest.Plates[^1].Parts.Add(MakePart()); + Assert.Equal(0, nest.Plates[^1].Parts.Count); + } }