diff --git a/src/ZB.MOM.WW.MxGateway.Worker.Tests/Probes/AlarmSubtagLiveSmokeTests.cs b/src/ZB.MOM.WW.MxGateway.Worker.Tests/Probes/AlarmSubtagLiveSmokeTests.cs
index 9fc9d88..43a1bae 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker.Tests/Probes/AlarmSubtagLiveSmokeTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker.Tests/Probes/AlarmSubtagLiveSmokeTests.cs
@@ -34,6 +34,7 @@ using System.Runtime.InteropServices;
using System.Threading;
using ZB.MOM.WW.MxGateway.Contracts.Proto;
using ZB.MOM.WW.MxGateway.Worker.MxAccess;
+using ZB.MOM.WW.MxGateway.Worker.Tests.TestSupport;
using Xunit.Abstractions;
namespace ZB.MOM.WW.MxGateway.Worker.Tests.Probes;
@@ -146,6 +147,138 @@ public sealed class AlarmSubtagLiveSmokeTests
}
}
+ ///
+ /// Runtime-path resolution probe. Unlike the full lifecycle test this
+ /// does NOT require an active alarm: it advises the four subtags and
+ /// observes the initial values MXAccess delivers on advise, then writes
+ /// the AckMsg subtag. It confirms the runtime item-reference path
+ /// (<Object>.<AlarmAttr>.<field>) resolves with
+ /// no intermediate alarm-condition segment — the last open item from the
+ /// design. Runs only when MXGATEWAY_RUN_LIVE_MXACCESS_TESTS=1.
+ /// Reports "inconclusive" (without failing) when no values are delivered,
+ /// e.g. the Galaxy engine for the test object is not running.
+ ///
+ [LiveMxAccessFact]
+ public void LiveProbe_AlarmSubtagsResolve_AndAckMsgWriteSucceeds()
+ {
+ Exception? threadException = null;
+ ManualResetEventSlim done = new ManualResetEventSlim(false);
+ Thread thread = new Thread(() =>
+ {
+ try { RunProbe(); }
+ catch (Exception ex) { threadException = ex; }
+ finally { done.Set(); }
+ });
+ thread.IsBackground = false;
+ thread.SetApartmentState(ApartmentState.STA);
+ thread.Start();
+ done.Wait();
+ thread.Join();
+
+ output.WriteLine(string.Format("Captured {0} log line(s):", log.Count));
+ string? line;
+ while (log.TryDequeue(out line))
+ {
+ output.WriteLine(line);
+ }
+
+ if (threadException != null)
+ {
+ throw threadException;
+ }
+ }
+
+ private void RunProbe()
+ {
+ Log("=== Subtag runtime-path resolution probe ===");
+ Log("AlarmFullReference: " + AlarmFullReference);
+ Log("Subtag addresses under test:");
+ Log(" active = " + PlaceholderActiveSubtag);
+ Log(" acked = " + PlaceholderAckedSubtag);
+ Log(" ackMsg = " + PlaceholderAckCommentSubtag);
+ Log(" priority= " + PlaceholderPrioritySubtag);
+
+ AlarmSubtagTarget target = new AlarmSubtagTarget
+ {
+ AlarmFullReference = AlarmFullReference,
+ SourceObjectReference = AlarmTagName,
+ ActiveSubtag = PlaceholderActiveSubtag,
+ AckedSubtag = PlaceholderAckedSubtag,
+ AckCommentSubtag = PlaceholderAckCommentSubtag,
+ PrioritySubtag = PlaceholderPrioritySubtag,
+ };
+ List watchList = new List { target };
+
+ MxAccessComObjectFactory factory = new MxAccessComObjectFactory();
+ LmxSubtagAlarmSource source = new LmxSubtagAlarmSource(factory, clientName: null);
+ using SubtagAlarmConsumer consumer = new SubtagAlarmConsumer(source, watchList);
+
+ // Hook the RAW source so every advised subtag's value-change is seen,
+ // including the initial value MXAccess delivers on advise (the state
+ // machine itself only emits on active/acked transitions).
+ Dictionary lastValues =
+ new Dictionary(StringComparer.OrdinalIgnoreCase);
+ source.ValueChanged += (_, c) =>
+ {
+ string v = c.Value == null
+ ? ""
+ : string.Format("{0} ({1})", c.Value, c.Value.GetType().Name);
+ lastValues[c.ItemAddress] = v;
+ Log(string.Format("ValueChanged: {0} = {1}", c.ItemAddress, v));
+ };
+
+ Log("Subscribe (advise InAlarm/Acked/Priority) ...");
+ consumer.Subscribe(string.Format(@"\\{0}\Galaxy!{1}", Environment.MachineName, Group));
+ Log("Subscribe returned OK.");
+
+ // Pump the STA for ~6s to receive each advised subtag's initial value.
+ DateTime deadline = DateTime.UtcNow + TimeSpan.FromSeconds(6);
+ while (DateTime.UtcNow < deadline)
+ {
+ PumpMessages();
+ Thread.Sleep(100);
+ }
+
+ Log(string.Format("Distinct subtags that delivered a value: {0}", lastValues.Count));
+ bool inAlarmResolved = lastValues.ContainsKey(PlaceholderActiveSubtag);
+ Log("InAlarm subtag delivered a value: " + inAlarmResolved);
+
+ // Decisive write: AckMsg. Returns 0 only if the address resolves and is writable.
+ Log("AcknowledgeByName (writes AckMsg subtag) ...");
+ int rc = consumer.AcknowledgeByName(
+ alarmName: AlarmTagName,
+ providerName: Provider,
+ groupName: Group,
+ ackComment: "subtag-resolution-probe",
+ ackOperatorName: Environment.UserName,
+ ackOperatorNode: Environment.MachineName,
+ ackOperatorDomain: Environment.UserDomainName ?? string.Empty,
+ ackOperatorFullName: Environment.UserName);
+ Log(string.Format("AcknowledgeByName(AckMsg write) -> rc={0}", rc));
+
+ DateTime settle = DateTime.UtcNow + TimeSpan.FromSeconds(2);
+ while (DateTime.UtcNow < settle)
+ {
+ PumpMessages();
+ Thread.Sleep(100);
+ }
+
+ if (lastValues.Count == 0)
+ {
+ Log("INCONCLUSIVE: no subtag values delivered within the window. The Galaxy "
+ + "engine hosting TestMachine_001 may be stopped, or the runtime path needs "
+ + "an intermediate segment. Re-run with the engine running. (Field names "
+ + "remain confirmed from the ZB AlarmExtension model.)");
+ return;
+ }
+
+ // Live data arrived — assert the in-alarm subtag resolved and the ack write succeeded.
+ Assert.True(inAlarmResolved,
+ "InAlarm subtag delivered no value while other subtags did — the runtime path for InAlarm may differ.");
+ Assert.Equal(0, rc);
+ Log("CONFIRMED: runtime subtag path resolves and the AckMsg write succeeded.");
+ }
+
private void RunSmoke()
{
Log(string.Format("AlarmFullReference: {0}", AlarmFullReference));