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
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).
83 lines
4.1 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|