using ZB.MOM.WW.MxGateway.Client; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Runtime; namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests.Runtime; /// /// D.1 smoke (alarm-source leg): drives the REAL gateway StreamAlarms feed through the /// production lmxopcua consumer () and asserts native alarm /// transitions — with operator comment, category, original raise time, and the mapped OPC UA /// severity bucket preserved — reach the driver-side boundary that feeds /// IAlarmSource.OnAlarmEvent. /// /// Skip-gated: runs only when MXGW_ENDPOINT + GALAXY_MXGW_API_KEY are set to a /// reachable gateway. Captured 2026-05-29 against 10.100.0.48:5120 — see /// docs/plans/alarms-d1-smoke-artifact.md. Set D1_SMOKE_OUT to dump the observed /// transitions to a file for artifact capture. /// /// [Trait("Category", "Integration")] public sealed class GatewayGalaxyAlarmFeedLiveTests { [Fact] public async Task Live_gateway_delivers_native_alarm_transitions_through_the_consumer() { var endpoint = Environment.GetEnvironmentVariable("MXGW_ENDPOINT"); var apiKey = Environment.GetEnvironmentVariable("GALAXY_MXGW_API_KEY"); if (string.IsNullOrWhiteSpace(endpoint) || string.IsNullOrWhiteSpace(apiKey)) Assert.Skip("Set MXGW_ENDPOINT + GALAXY_MXGW_API_KEY to run the live gateway alarm-feed smoke."); var client = MxGatewayClient.Create(new MxGatewayClientOptions { Endpoint = new Uri(endpoint!, UriKind.Absolute), ApiKey = apiKey!, UseTls = false, ConnectTimeout = TimeSpan.FromSeconds(10), DefaultCallTimeout = TimeSpan.FromSeconds(30), StreamTimeout = TimeSpan.FromSeconds(30), }); var observed = new List(); var gotOne = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); // Wire the live client's StreamAlarms method group into the production consumer seam. await using var feed = new GatewayGalaxyAlarmFeed(client.StreamAlarmsAsync, clientName: "D1Smoke"); feed.OnAlarmTransition += (_, t) => { lock (observed) { observed.Add(t); } gotOne.TrySetResult(true); }; feed.Start(); // The stream opens with the active-alarm snapshot, so we expect ≥1 transition promptly. await Task.WhenAny(gotOne.Task, Task.Delay(TimeSpan.FromSeconds(20), TestContext.Current.CancellationToken)); List snapshot; lock (observed) snapshot = observed.ToList(); snapshot.ShouldNotBeEmpty( "Live gateway should deliver at least the active-alarm snapshot through the lmxopcua consumer."); var first = snapshot[0]; first.AlarmFullReference.ShouldNotBeNullOrWhiteSpace(); first.OpcUaSeverity.ShouldBeGreaterThan(0); // severity bucket mapping applied by the consumer foreach (var t in snapshot.Take(8)) TestContext.Current.SendDiagnosticMessage( $"{t.TransitionKind,-11} {t.AlarmFullReference} sev={t.OpcUaSeverity}({t.SeverityBucket}) cat={t.Category} comment='{t.OperatorComment}'"); TestContext.Current.SendDiagnosticMessage($"TOTAL consumer transitions observed: {snapshot.Count}"); // Deterministic artifact capture (only when D1_SMOKE_OUT is set). var outPath = Environment.GetEnvironmentVariable("D1_SMOKE_OUT"); if (!string.IsNullOrWhiteSpace(outPath)) { var lines = snapshot.Take(50).Select(t => $"{t.TransitionKind,-11} {t.AlarmFullReference} | sev={t.OpcUaSeverity}({t.SeverityBucket}) raw={t.RawMxAccessSeverity} | cat={t.Category} | comment='{t.OperatorComment}' | xitionUtc={t.TransitionTimestampUtc:o}"); await File.WriteAllLinesAsync(outPath!, new[] { $"# consumer transitions observed: {snapshot.Count}" }.Concat(lines), TestContext.Current.CancellationToken); } } }