feat: implement PlateManager sentinel, reactive subscriptions, and batch ops (Tasks 3-5)
- EnsureSentinel() maintains exactly one trailing empty plate, suppressing navigation events during mutation - Reactive tail subscriptions (PartAdded/PartRemoved on last two plates) call EnsureSentinel automatically; re-subscribed after each plate list change - BeginBatch()/EndBatch() defers sentinel enforcement during bulk operations - GetOrCreateEmpty() returns or creates an empty plate; RemoveCurrent() removes the current plate with index clamping; CanRemoveCurrent guards deletion - 13 new tests (30 total PlateManager tests), all passing Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -205,4 +205,191 @@ public class PlateManagerTests
|
||||
nest.CreatePlate();
|
||||
Assert.False(eventFired);
|
||||
}
|
||||
|
||||
// Task 3: Sentinel plate invariant
|
||||
|
||||
[Fact]
|
||||
public void EnsureSentinel_EmptyNest_CreatesOnePlate()
|
||||
{
|
||||
var nest = CreateNest();
|
||||
using var mgr = new PlateManager(nest);
|
||||
mgr.EnsureSentinel();
|
||||
Assert.Equal(1, nest.Plates.Count);
|
||||
Assert.Equal(0, nest.Plates[0].Parts.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnsureSentinel_LastPlateHasParts_CreatesNewEmpty()
|
||||
{
|
||||
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);
|
||||
Assert.Equal(0, nest.Plates[^1].Parts.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnsureSentinel_TwoTrailingEmpties_TrimsToOne()
|
||||
{
|
||||
var nest = CreateNest();
|
||||
nest.CreatePlate();
|
||||
nest.CreatePlate();
|
||||
using var mgr = new PlateManager(nest);
|
||||
mgr.EnsureSentinel();
|
||||
Assert.Equal(1, nest.Plates.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnsureSentinel_PreservesCurrentIndex()
|
||||
{
|
||||
var nest = CreateNest();
|
||||
var plate = nest.CreatePlate();
|
||||
plate.Parts.Add(MakePart());
|
||||
using var mgr = new PlateManager(nest);
|
||||
mgr.EnsureSentinel();
|
||||
Assert.Equal(0, mgr.CurrentIndex);
|
||||
Assert.Same(plate, mgr.CurrentPlate);
|
||||
}
|
||||
|
||||
// Task 4: Reactive tail-plate subscriptions
|
||||
|
||||
[Fact]
|
||||
public void PartAddedToLastPlate_CreatesSentinel()
|
||||
{
|
||||
var nest = CreateNest();
|
||||
nest.CreatePlate();
|
||||
using var mgr = new PlateManager(nest);
|
||||
mgr.EnsureSentinel();
|
||||
var initialCount = nest.Plates.Count;
|
||||
nest.Plates[^1].Parts.Add(MakePart());
|
||||
Assert.Equal(initialCount + 1, nest.Plates.Count);
|
||||
Assert.Equal(0, nest.Plates[^1].Parts.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PartRemovedFromSecondToLast_TrimsExtraEmpty()
|
||||
{
|
||||
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);
|
||||
nest.Plates[0].Parts.RemoveAt(0);
|
||||
Assert.Equal(1, nest.Plates.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReactiveSubscription_ResubscribesAfterPlateListChange()
|
||||
{
|
||||
var nest = CreateNest();
|
||||
var plate1 = nest.CreatePlate();
|
||||
plate1.Parts.Add(MakePart());
|
||||
using var mgr = new PlateManager(nest);
|
||||
mgr.EnsureSentinel();
|
||||
nest.Plates[^1].Parts.Add(MakePart());
|
||||
Assert.Equal(3, nest.Plates.Count);
|
||||
nest.Plates[^1].Parts.Add(MakePart());
|
||||
Assert.Equal(4, nest.Plates.Count);
|
||||
Assert.Equal(0, nest.Plates[^1].Parts.Count);
|
||||
}
|
||||
|
||||
// Task 5: Batch mode and plate operations
|
||||
|
||||
[Fact]
|
||||
public void BeginBatch_DefersSentinelEnforcement()
|
||||
{
|
||||
var nest = CreateNest();
|
||||
var plate = nest.CreatePlate();
|
||||
plate.Parts.Add(MakePart());
|
||||
using var mgr = new PlateManager(nest);
|
||||
mgr.EnsureSentinel();
|
||||
|
||||
mgr.BeginBatch();
|
||||
nest.Plates[^1].Parts.Add(MakePart());
|
||||
Assert.Equal(2, nest.Plates.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EndBatch_EnforcesSentinelAndFiresEvents()
|
||||
{
|
||||
var nest = CreateNest();
|
||||
var plate = nest.CreatePlate();
|
||||
plate.Parts.Add(MakePart());
|
||||
using var mgr = new PlateManager(nest);
|
||||
mgr.EnsureSentinel();
|
||||
|
||||
mgr.BeginBatch();
|
||||
nest.Plates[^1].Parts.Add(MakePart());
|
||||
|
||||
var listChangedCount = 0;
|
||||
mgr.PlateListChanged += (s, e) => listChangedCount++;
|
||||
|
||||
mgr.EndBatch();
|
||||
|
||||
Assert.Equal(3, nest.Plates.Count);
|
||||
Assert.Equal(0, nest.Plates[^1].Parts.Count);
|
||||
Assert.True(listChangedCount > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetOrCreateEmpty_ReturnsSentinelWhenEmpty()
|
||||
{
|
||||
var nest = CreateNest();
|
||||
var plate1 = nest.CreatePlate();
|
||||
plate1.Parts.Add(MakePart());
|
||||
using var mgr = new PlateManager(nest);
|
||||
mgr.EnsureSentinel();
|
||||
|
||||
var result = mgr.GetOrCreateEmpty();
|
||||
Assert.Same(nest.Plates[^1], result);
|
||||
Assert.Equal(0, result.Parts.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetOrCreateEmpty_NoEmptyPlate_CreatesNew()
|
||||
{
|
||||
var nest = CreateNest();
|
||||
var plate1 = nest.CreatePlate();
|
||||
plate1.Parts.Add(MakePart());
|
||||
using var mgr = new PlateManager(nest);
|
||||
|
||||
var result = mgr.GetOrCreateEmpty();
|
||||
Assert.Equal(0, result.Parts.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoveCurrent_RemovesPlateAndClampsIndex()
|
||||
{
|
||||
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.LoadLast();
|
||||
|
||||
mgr.RemoveCurrent();
|
||||
|
||||
Assert.Equal(1, nest.Plates.Count);
|
||||
Assert.Equal(0, mgr.CurrentIndex);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanRemoveCurrent_OnlyIfMultiplePlatesAndHasParts()
|
||||
{
|
||||
var nest = CreateNest();
|
||||
var plate1 = nest.CreatePlate();
|
||||
using var mgr = new PlateManager(nest);
|
||||
|
||||
Assert.False(mgr.CanRemoveCurrent);
|
||||
|
||||
plate1.Parts.Add(MakePart());
|
||||
Assert.False(mgr.CanRemoveCurrent);
|
||||
|
||||
nest.CreatePlate();
|
||||
Assert.True(mgr.CanRemoveCurrent);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user