From 46aba992c5a50b9601218372a870a4dda5f3a123 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sun, 7 Jun 2026 10:47:33 -0400 Subject: [PATCH] fix(config): DraftSnapshotFactory loads only active (unreleased) reservations Filter ExternalIdReservations to WHERE ReleasedAt IS NULL so DraftSnapshot.ActiveReservations matches its documented semantics and ValidateReservationPreflight cannot emit spurious BadDuplicateExternalIdentifier errors from already-released rows. Adds a focused unit test seeding one active and one released reservation and asserting only the active row is returned. --- .../Validation/DraftSnapshotFactory.cs | 5 ++- .../DraftSnapshotFactoryTests.cs | 38 +++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Validation/DraftSnapshotFactory.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Validation/DraftSnapshotFactory.cs index 50cf4b68..1b5e019d 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Validation/DraftSnapshotFactory.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Validation/DraftSnapshotFactory.cs @@ -43,6 +43,9 @@ public static class DraftSnapshotFactory VirtualTags = await db.VirtualTags.AsNoTracking().ToListAsync(ct), PollGroups = await db.PollGroups.AsNoTracking().ToListAsync(ct), PriorEquipment = [], - ActiveReservations = await db.ExternalIdReservations.AsNoTracking().ToListAsync(ct), + ActiveReservations = await db.ExternalIdReservations + .AsNoTracking() + .Where(r => r.ReleasedAt == null) // active only — matches DraftSnapshot.ActiveReservations semantics + .ToListAsync(ct), }; } diff --git a/tests/Core/ZB.MOM.WW.OtOpcUa.Configuration.Tests/DraftSnapshotFactoryTests.cs b/tests/Core/ZB.MOM.WW.OtOpcUa.Configuration.Tests/DraftSnapshotFactoryTests.cs index c3e44ba6..c58a2ee5 100644 --- a/tests/Core/ZB.MOM.WW.OtOpcUa.Configuration.Tests/DraftSnapshotFactoryTests.cs +++ b/tests/Core/ZB.MOM.WW.OtOpcUa.Configuration.Tests/DraftSnapshotFactoryTests.cs @@ -63,6 +63,44 @@ public sealed class DraftSnapshotFactoryTests : IDisposable DraftValidator.Validate(snapshot).ShouldNotContain(e => e.Code == "EquipmentSignalNameCollision"); } + /// Seeds one active and one released reservation for the same equipment context; + /// only the active row (ReleasedAt == null) should appear in the snapshot. + [Fact] + public async Task FromConfigDb_ActiveReservations_excludes_released_rows() + { + var equipmentUuid = Guid.NewGuid(); + + _db.ExternalIdReservations.Add(new ExternalIdReservation + { + ReservationId = Guid.NewGuid(), + Kind = ReservationKind.ZTag, + Value = "ZT-001", + EquipmentUuid = equipmentUuid, + ClusterId = "cluster-a", + FirstPublishedBy = "test", + ReleasedAt = null, // active + }); + _db.ExternalIdReservations.Add(new ExternalIdReservation + { + ReservationId = Guid.NewGuid(), + Kind = ReservationKind.ZTag, + Value = "ZT-002", + EquipmentUuid = equipmentUuid, + ClusterId = "cluster-a", + FirstPublishedBy = "test", + ReleasedAt = new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc), // released + ReleasedBy = "test", + ReleaseReason = "retired", + }); + await _db.SaveChangesAsync(); + + var snapshot = await DraftSnapshotFactory.FromConfigDbAsync(_db); + + snapshot.ActiveReservations.Count.ShouldBe(1); + snapshot.ActiveReservations[0].Value.ShouldBe("ZT-001"); + snapshot.ActiveReservations[0].ReleasedAt.ShouldBeNull(); + } + private void SeedEquipment(string equipmentId) { var uuid = Guid.NewGuid();