using Akka.TestKit.Xunit2; using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums; using ZB.MOM.WW.ScadaBridge.SiteRuntime.Actors; using ZB.MOM.WW.ScadaBridge.StoreAndForward; namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests; /// /// Characterization pin for the chunked anti-entropy resync contract (review 02 round 2, /// N2). (active→standby) and /// (standby→active) ride intra-site Akka remoting on the default reflective-JSON wire /// format. A rename/move, dropped setter, or non-default-constructible message would /// silently break resync across a rolling upgrade and only surface as a divergent buffer /// after a failover. These pin round-trip fidelity and type identity. (These messages are /// NOT ClusterClient traffic, so they are intentionally absent from ClusterClientContractLockTests.) /// public class ResyncWireSerializationPinTests : TestKit { private T RoundTrip(T message) { var serialization = Sys.Serialization; var serializer = serialization.FindSerializerFor(message); var bytes = serializer.ToBinary(message); return (T)serialization.Deserialize(bytes, serializer.Identifier, message!.GetType()); } private static StoreAndForwardMessage FullMessage() => new() { Id = Guid.NewGuid().ToString("N"), Category = StoreAndForwardCategory.Notification, Target = "Operators", PayloadJson = "{\"notificationId\":\"abc\"}", RetryCount = 4, MaxRetries = 0, RetryIntervalMs = 30000, CreatedAt = DateTimeOffset.UtcNow, LastAttemptAt = DateTimeOffset.UtcNow, Status = StoreAndForwardMessageStatus.Parked, LastError = "central rejected", OriginInstanceName = "Plant.Pump3", ExecutionId = Guid.NewGuid(), SourceScript = "ScriptActor:MonitorSpeed", ParentExecutionId = Guid.NewGuid(), }; [Fact] public void SfBufferSnapshotChunk_WithFullMessage_RoundTripsOnTheWire() { var message = FullMessage(); var original = new SfBufferSnapshotChunk( "resync-1", 2, 5, new List { message }, Truncated: true); var back = RoundTrip(original); Assert.Equal(original.ResyncId, back.ResyncId); Assert.Equal(original.Sequence, back.Sequence); Assert.Equal(original.TotalChunks, back.TotalChunks); Assert.Equal(original.Truncated, back.Truncated); var m = Assert.Single(back.Messages); Assert.Equal(message.Id, m.Id); Assert.Equal(message.Category, m.Category); Assert.Equal(message.Target, m.Target); Assert.Equal(message.PayloadJson, m.PayloadJson); Assert.Equal(message.RetryCount, m.RetryCount); Assert.Equal(message.MaxRetries, m.MaxRetries); Assert.Equal(message.RetryIntervalMs, m.RetryIntervalMs); Assert.Equal(message.CreatedAt, m.CreatedAt); Assert.Equal(message.LastAttemptAt, m.LastAttemptAt); Assert.Equal(message.Status, m.Status); Assert.Equal(message.LastError, m.LastError); Assert.Equal(message.OriginInstanceName, m.OriginInstanceName); Assert.Equal(message.ExecutionId, m.ExecutionId); Assert.Equal(message.SourceScript, m.SourceScript); Assert.Equal(message.ParentExecutionId, m.ParentExecutionId); } [Fact] public void SfBufferResyncAck_RoundTripsOnTheWire() { var original = new SfBufferResyncAck("resync-1", 42); var back = RoundTrip(original); Assert.Equal(original.ResyncId, back.ResyncId); Assert.Equal(original.RowCount, back.RowCount); } // Type-identity pins: the reflective-JSON wire embeds CLR type manifests, so a // rename/move of either type silently breaks resync across a rolling upgrade. [Theory] [InlineData(typeof(SfBufferSnapshotChunk), "ZB.MOM.WW.ScadaBridge.SiteRuntime.Actors.SfBufferSnapshotChunk")] [InlineData(typeof(SfBufferResyncAck), "ZB.MOM.WW.ScadaBridge.SiteRuntime.Actors.SfBufferResyncAck")] public void ResyncContract_TypeIdentity_IsPinned(Type type, string expectedFullName) => Assert.Equal(expectedFullName, type.FullName); }