using ZB.MOM.WW.MxGateway.Client;
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.Runtime;
///
/// Live meter legs for the 06/S-1 write-confirmation gate — the observation that closed the
/// archreview finding: against a correlated gateway (mxaccessgw server ≥ b948e69, worker ≥
/// 53f69cd) every unary write reply carries the worker's OnWriteComplete outcome as
/// statuses[0], so galaxy.writes.unconfirmed — which used to increment on EVERY
/// write — stays flat, and only a completion-wait timeout (or a pre-correlation gateway) meters.
///
/// ⚠️ Run these FILTERED (one test class), never as part of a full-assembly run with the env vars
/// set: the offline exercise the unconfirmed path
/// against fakes, and xunit runs test classes in parallel, so their increments would pollute the
/// process-wide counters these tests listen to.
///
///
[Trait("Category", "Integration")]
public sealed class GatewayGalaxyWriteMeterLiveTests
{
private const string WriteRef = "TestMachine_002.TestFloat";
///
/// Confirmed writes keep both write meters flat: five committed writes and one refusal, all
/// carrying a correlated status row — zero unconfirmed (the counter is presence-based,
/// so the refusal must not increment it either) and zero advise_failed.
///
[Fact]
public async Task Live_confirmed_writes_keep_unconfirmed_meter_flat()
{
var (endpoint, apiKey) = RequireLiveGatewayOrSkip();
long unconfirmed = 0;
long adviseFailed = 0;
using var listener = new System.Diagnostics.Metrics.MeterListener();
listener.InstrumentPublished = (instrument, l) =>
{
if (instrument.Name is "galaxy.writes.unconfirmed" or "galaxy.writes.advise_failed")
l.EnableMeasurementEvents(instrument);
};
listener.SetMeasurementEventCallback((instrument, value, _, _) =>
{
if (instrument.Name == "galaxy.writes.unconfirmed") Interlocked.Add(ref unconfirmed, value);
else Interlocked.Add(ref adviseFailed, value);
});
listener.Start();
var ct = TestContext.Current.CancellationToken;
await using var session = new GalaxyMxSession(new GalaxyMxAccessOptions(ClientName: "OtOpcUa06S1Meter"));
await session.ConnectAsync(BuildClientOptions(endpoint, apiKey), ct);
var writer = new GatewayGalaxyDataWriter(session, writeUserId: 0);
for (var i = 0; i < 5; i++)
{
var r = await writer.WriteAsync(
[new WriteRequest(WriteRef, 7000.0f + i)], _ => SecurityClassification.FreeAccess, ct);
Assert.Equal(0u, r[0].StatusCode);
}
var refused = await writer.WriteAsync(
[new WriteRequest("TestMachine_002.Tagname", "meter-leg-refusal")],
_ => SecurityClassification.FreeAccess, ct);
Assert.Equal(2u, (refused[0].StatusCode >> 30) & 0x3u);
listener.Dispose();
Assert.Equal(0, Interlocked.Read(ref unconfirmed));
Assert.Equal(0, Interlocked.Read(ref adviseFailed));
}
///
/// Timeout-control leg — run ONLY while the target gateway has the completion wait disabled
/// (MxGateway__Worker__WriteCompletionWaitMilliseconds=0, an NSSM env-var flip +
/// service restart on a deployed instance): every reply is then empty-statuses, so the writer
/// reports provisional Good and meters each write unconfirmed. Gated on
/// MXGW_WRITE_WAIT_DISABLED=1 so it can never run (and fail) against a
/// normally-configured gateway. Remove the override (same nssm command with -) and
/// restart afterwards;
/// passing again proves the restoration.
///
[Fact]
public async Task Live_wait_disabled_gateway_yields_provisional_good_and_metered_unconfirmed()
{
if (Environment.GetEnvironmentVariable("MXGW_WRITE_WAIT_DISABLED") != "1")
Assert.Skip("Set MXGW_WRITE_WAIT_DISABLED=1 (with the gateway's completion wait set to 0) to run the timeout leg.");
var (endpoint, apiKey) = RequireLiveGatewayOrSkip();
long unconfirmed = 0;
using var listener = new System.Diagnostics.Metrics.MeterListener();
listener.InstrumentPublished = (instrument, l) =>
{
if (instrument.Name == "galaxy.writes.unconfirmed") l.EnableMeasurementEvents(instrument);
};
listener.SetMeasurementEventCallback((_, value, _, _) => Interlocked.Add(ref unconfirmed, value));
listener.Start();
var ct = TestContext.Current.CancellationToken;
await using var session = new GalaxyMxSession(new GalaxyMxAccessOptions(ClientName: "OtOpcUa06S1Leg3"));
await session.ConnectAsync(BuildClientOptions(endpoint, apiKey), ct);
var writer = new GatewayGalaxyDataWriter(session, writeUserId: 0);
for (var i = 0; i < 3; i++)
{
var r = await writer.WriteAsync(
[new WriteRequest(WriteRef, 7100.0f + i)], _ => SecurityClassification.FreeAccess, ct);
Assert.Equal(0u, r[0].StatusCode); // provisional Good off the empty-statuses reply
}
listener.Dispose();
Assert.Equal(3, Interlocked.Read(ref unconfirmed));
}
private static (string Endpoint, string ApiKey) RequireLiveGatewayOrSkip()
{
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 write-meter legs.");
return (endpoint!, apiKey!);
}
private static MxGatewayClientOptions BuildClientOptions(string endpoint, string apiKey) => new()
{
Endpoint = new Uri(endpoint, UriKind.Absolute),
ApiKey = apiKey,
UseTls = false,
ConnectTimeout = TimeSpan.FromSeconds(10),
DefaultCallTimeout = TimeSpan.FromSeconds(30),
StreamTimeout = TimeSpan.FromSeconds(30),
};
}