Files
lmxopcua/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests/Runtime/GatewayGalaxyAlarmFeedLiveTests.cs
T
Joseph Doherty 695fa6408b
v2-ci / build (push) Failing after 47s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
docs(alarms): record native alarms verified working; add D.1 smoke
The 2026-04-30 alarm plan banners claimed worker-side native alarm
subscription was blocked on a COM-bitness finding. That's stale: the
mxaccessgw .NET client now has true MxAccess alarm-event support, and a
live StreamAlarms check (+ new Skip-gated GatewayGalaxyAlarmFeedLiveTests
through the lmxopcua consumer) confirms native alarms — operator comment,
category, severity, timestamps — flow end-to-end. Reconcile both plan docs
to reality and add docs/plans/alarms-d1-smoke-artifact.md as the D.1
alarm-source deliverable. Historian-write live smoke + full server->A&C
round-trip remain (Windows parity rig only).
2026-05-31 09:59:01 -04:00

83 lines
4.1 KiB
C#

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;
/// <summary>
/// D.1 smoke (alarm-source leg): drives the REAL gateway <c>StreamAlarms</c> feed through the
/// production lmxopcua consumer (<see cref="GatewayGalaxyAlarmFeed"/>) 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
/// <c>IAlarmSource.OnAlarmEvent</c>.
/// <para>
/// Skip-gated: runs only when <c>MXGW_ENDPOINT</c> + <c>GALAXY_MXGW_API_KEY</c> are set to a
/// reachable gateway. Captured 2026-05-29 against <c>10.100.0.48:5120</c> — see
/// <c>docs/plans/alarms-d1-smoke-artifact.md</c>. Set <c>D1_SMOKE_OUT</c> to dump the observed
/// transitions to a file for artifact capture.
/// </para>
/// </summary>
[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<GalaxyAlarmTransition>();
var gotOne = new TaskCompletionSource<bool>(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<GalaxyAlarmTransition> 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);
}
}
}