using System.Threading.Channels; using Google.Protobuf.WellKnownTypes; using MxGateway.Contracts.Proto; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Config; using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Runtime; namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests; /// /// PR E.7 — pins that the GalaxyDriver populates the extended AlarmEventArgs /// fields (OperatorComment, OriginalRaiseTimestampUtc, AlarmCategory) when the /// gateway emits a transition with the rich payload, and leaves them null on /// events that don't carry them. /// public sealed class GalaxyDriverAlarmEventArgsExtensionTests { [Fact] public async Task Acknowledge_transition_with_full_payload_populates_extended_fields() { var subscriber = new ManualSubscriber(); using var driver = NewDriver(subscriber); await driver.SubscribeAlarmsAsync(["Tank01"], CancellationToken.None); var observed = new List(); driver.OnAlarmEvent += (_, args) => observed.Add(args); await driver.SubscribeAsync(["Tank01.Level"], TimeSpan.Zero, CancellationToken.None); var raise = new DateTime(2026, 5, 1, 12, 0, 0, DateTimeKind.Utc); var ack = raise.AddSeconds(45); await subscriber.EmitAlarmAsync(new MxEvent { Family = MxEventFamily.OnAlarmTransition, OnAlarmTransition = new OnAlarmTransitionEvent { AlarmFullReference = "Tank01.Level.HiHi", SourceObjectReference = "Tank01", AlarmTypeName = "AnalogLimitAlarm.HiHi", TransitionKind = AlarmTransitionKind.Acknowledge, Severity = 750, OriginalRaiseTimestamp = Timestamp.FromDateTime(raise), TransitionTimestamp = Timestamp.FromDateTime(ack), OperatorUser = "alice", OperatorComment = "investigating", Category = "Process", Description = "Tank 01 high-high level", }, }); for (var i = 0; i < 20 && observed.Count == 0; i++) { await Task.Delay(50); } observed.ShouldHaveSingleItem(); observed[0].OperatorComment.ShouldBe("investigating"); observed[0].OriginalRaiseTimestampUtc.ShouldBe(raise); observed[0].AlarmCategory.ShouldBe("Process"); } [Fact] public async Task Raise_transition_without_optional_fields_leaves_them_null() { var subscriber = new ManualSubscriber(); using var driver = NewDriver(subscriber); await driver.SubscribeAlarmsAsync(["Tank01"], CancellationToken.None); var observed = new List(); driver.OnAlarmEvent += (_, args) => observed.Add(args); await driver.SubscribeAsync(["Tank01.Level"], TimeSpan.Zero, CancellationToken.None); await subscriber.EmitAlarmAsync(new MxEvent { Family = MxEventFamily.OnAlarmTransition, OnAlarmTransition = new OnAlarmTransitionEvent { AlarmFullReference = "Tank01.Level.HiHi", AlarmTypeName = "AnalogLimitAlarm.HiHi", TransitionKind = AlarmTransitionKind.Raise, Severity = 750, TransitionTimestamp = Timestamp.FromDateTime(DateTime.UtcNow), }, }); for (var i = 0; i < 20 && observed.Count == 0; i++) { await Task.Delay(50); } observed.ShouldHaveSingleItem(); observed[0].OperatorComment.ShouldBeNull(); observed[0].OriginalRaiseTimestampUtc.ShouldBeNull(); observed[0].AlarmCategory.ShouldBeNull(); } private static GalaxyDriver NewDriver(ManualSubscriber subscriber) { var options = new GalaxyDriverOptions( new GalaxyGatewayOptions("http://localhost:5000", "literal-api-key"), new GalaxyMxAccessOptions("AlarmExtensionTest"), new GalaxyRepositoryOptions(), new GalaxyReconnectOptions()); return new GalaxyDriver( driverInstanceId: "drv-1", options: options, hierarchySource: null, dataReader: null, dataWriter: null, subscriber: subscriber, alarmAcknowledger: null); } private sealed class ManualSubscriber : IGalaxySubscriber { private readonly Channel _stream = Channel.CreateUnbounded(new UnboundedChannelOptions { SingleReader = true }); public Task> SubscribeBulkAsync( IReadOnlyList fullReferences, int bufferedUpdateIntervalMs, CancellationToken cancellationToken) { var results = new List(); var nextHandle = 100; foreach (var r in fullReferences) { results.Add(new SubscribeResult { TagAddress = r, ItemHandle = nextHandle++, WasSuccessful = true }); } return Task.FromResult>(results); } public Task UnsubscribeBulkAsync(IReadOnlyList itemHandles, CancellationToken cancellationToken) => Task.CompletedTask; public IAsyncEnumerable StreamEventsAsync(CancellationToken cancellationToken) => _stream.Reader.ReadAllAsync(cancellationToken); public ValueTask EmitAlarmAsync(MxEvent ev) => _stream.Writer.WriteAsync(ev); } }