using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Commons.Messages.Drivers; using ZB.MOM.WW.OtOpcUa.Runtime.Telemetry; namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Telemetry; /// /// Review 02 L4: 's tombstone set only ever shrank on a re-spawn of the /// SAME id, so a driver removed for good kept its id forever. The tombstone is load-bearing (#609 — it holds /// back the removed child's late writes), so it cannot simply be cleared; it is now timestamped and swept /// once older than , which outlives any in-flight /// capability call by a wide margin. /// public sealed class TelemetryLocalHubTombstoneSweepTests { private static DriverHealthChanged Health(string instanceId) => new("cluster-a", instanceId, "Healthy", null, null, 0, DateTime.UtcNow); private static List Drain(ITelemetrySubscription sub) { var items = new List(); while (sub.Reader.TryRead(out var item)) items.Add(item); return items; } [Fact] public void A_tombstone_holds_late_writes_until_the_ttl_then_is_swept() { var clock = new ManualTime { Now = new DateTimeOffset(2026, 10, 22, 12, 0, 0, TimeSpan.Zero) }; var hub = new TelemetryLocalHub(clock); hub.Emit(new TelemetryItem.Health(Health("drv-a"))); hub.Evict("drv-a"); // Inside the TTL: the late write is dropped (the #609 contract) — and another eviction's sweep does // not lift a tombstone that is still young. clock.Now += TelemetryLocalHub.EvictionTombstoneTtl / 2; hub.Evict("drv-b"); hub.Emit(new TelemetryItem.Health(Health("drv-a"))); using (var sub = hub.Subscribe(16)) Drain(sub).ShouldBeEmpty("a late write inside the TTL must still be held back"); // Past the TTL: the next eviction sweeps drv-a's tombstone and its writes land again. clock.Now += TelemetryLocalHub.EvictionTombstoneTtl; hub.Evict("drv-c"); hub.Emit(new TelemetryItem.Health(Health("drv-a"))); using (var sub = hub.Subscribe(16)) Drain(sub).OfType().Single().E.DriverInstanceId.ShouldBe("drv-a"); } [Fact] public void Readmit_lifts_a_tombstone_before_its_ttl() { var clock = new ManualTime { Now = DateTimeOffset.UtcNow }; var hub = new TelemetryLocalHub(clock); hub.Evict("drv-a"); hub.Readmit("drv-a"); hub.Emit(new TelemetryItem.Health(Health("drv-a"))); using var sub = hub.Subscribe(16); Drain(sub).Count.ShouldBe(1); } private sealed class ManualTime : TimeProvider { public DateTimeOffset Now { get; set; } public override DateTimeOffset GetUtcNow() => Now; } }