using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.ParityTests; /// /// PR 5.5 — Alarm-condition + transition parity. Both backends discover the /// same set of alarm-bearing attributes with matching /// metadata; transition events from a live alarm flap must arrive with matching /// severity, message, and source-node-id on each side. /// /// /// Alarm-event persistence parity (the SQLite store-and-forward → Wonderware /// historian event store path called out in the impl plan) is exercised /// end-to-end in PR 5.6 against the historian sidecar; here we focus on the /// in-process transition stream that emits. /// [Trait("Category", "ParityE2E")] [Collection(nameof(ParityCollection))] public sealed class AlarmTransitionParityTests { private readonly ParityHarness _h; public AlarmTransitionParityTests(ParityHarness h) => _h = h; [Fact] public async Task Discover_emits_same_AlarmConditionInfo_per_alarm_attribute() { _h.RequireBoth(); var snapshots = await _h.RunOnAvailableAsync(async (driver, ct) => { var b = new RecordingAddressSpaceBuilder(); await ((ITagDiscovery)driver).DiscoverAsync(b, ct); return b.AlarmConditions.ToDictionary( ac => ac.SourceNodeId, ac => ac.Info, StringComparer.OrdinalIgnoreCase); }, CancellationToken.None); var legacy = snapshots[ParityHarness.Backend.LegacyHost]; var mxgw = snapshots[ParityHarness.Backend.MxGateway]; if (legacy.Count == 0) { Assert.Skip("dev Galaxy has no alarm-marked attributes — alarm parity unverified for this rig"); } legacy.Keys.OrderBy(x => x, StringComparer.OrdinalIgnoreCase) .ShouldBe(mxgw.Keys.OrderBy(x => x, StringComparer.OrdinalIgnoreCase), "alarm source-node-id set must match across backends"); foreach (var kvp in legacy) { mxgw[kvp.Key].InitialSeverity.ShouldBe(kvp.Value.InitialSeverity, $"alarm severity parity for '{kvp.Key}'"); mxgw[kvp.Key].SourceName.ShouldBe(kvp.Value.SourceName, $"alarm SourceName parity for '{kvp.Key}'"); mxgw[kvp.Key].InAlarmRef.ShouldBe(kvp.Value.InAlarmRef, $"alarm InAlarmRef parity for '{kvp.Key}'"); mxgw[kvp.Key].DescAttrNameRef.ShouldBe(kvp.Value.DescAttrNameRef, $"alarm DescAttrNameRef parity for '{kvp.Key}'"); } } [Fact] public async Task Discover_marks_at_least_one_alarm_attribute_when_dev_Galaxy_has_alarms() { _h.RequireBoth(); var snapshots = await _h.RunOnAvailableAsync(async (driver, ct) => { var b = new RecordingAddressSpaceBuilder(); await ((ITagDiscovery)driver).DiscoverAsync(b, ct); return b.Variables.Count(v => v.AttributeInfo.IsAlarm); }, CancellationToken.None); // Soft pin — count must match across backends. Whether the count is non-zero // depends on the rig's Galaxy content, so we don't gate on a positive number. snapshots[ParityHarness.Backend.LegacyHost] .ShouldBe(snapshots[ParityHarness.Backend.MxGateway], "IsAlarm-marked variable count parity"); } }