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 41ff65a..a19ea85 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker.Tests/Probes/AlarmSubtagLiveSmokeTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker.Tests/Probes/AlarmSubtagLiveSmokeTests.cs
@@ -288,6 +288,97 @@ public sealed class AlarmSubtagLiveSmokeTests
Log("CONFIRMED: runtime subtag path resolves and the AckMsg write succeeded.");
}
+ ///
+ /// Operator-driven watch on TestMachine_001.TestAlarm002 (currently
+ /// inactive). Subscribes the subtag route, then waits for an operator to
+ /// toggle the alarm true (Raise) and back to false (Clear), asserting both
+ /// transitions are synthesized with the degraded flag + synthetic GUID.
+ /// Runs only with MXGATEWAY_RUN_LIVE_MXACCESS_TESTS=1; exits as soon
+ /// as both transitions are seen.
+ ///
+ [LiveMxAccessFact]
+ public void LiveWatch_TestAlarm002_RaiseThenClear()
+ {
+ Exception? threadException = null;
+ ManualResetEventSlim done = new ManualResetEventSlim(false);
+ Thread thread = new Thread(() =>
+ {
+ try { RunWatch002(); }
+ catch (Exception ex) { threadException = ex; }
+ finally { done.Set(); }
+ });
+ thread.IsBackground = false;
+ thread.SetApartmentState(ApartmentState.STA);
+ thread.Start();
+ done.Wait();
+ thread.Join();
+
+ List lines = new List();
+ string? line;
+ while (log.TryDequeue(out line)) { output.WriteLine(line); lines.Add(line); }
+ string logPath = Environment.GetEnvironmentVariable("PROBE_LOG")
+ ?? System.IO.Path.Combine(System.IO.Path.GetTempPath(), "alarm002-watch.log");
+ try { System.IO.File.WriteAllLines(logPath, lines); } catch { /* best effort */ }
+ if (threadException != null) { throw threadException; }
+ }
+
+ private void RunWatch002()
+ {
+ const string alarm = "TestMachine_001.TestAlarm002";
+ string fullRef = AlarmRecordTransitionMapper.ComposeFullReference(Provider, Group, alarm);
+ Log("=== Watch TestAlarm002 for operator-driven Raise + Clear ===");
+ Log("AlarmFullReference: " + fullRef);
+
+ AlarmSubtagTarget target = new AlarmSubtagTarget
+ {
+ AlarmFullReference = fullRef,
+ SourceObjectReference = alarm,
+ ActiveSubtag = alarm + ".InAlarm",
+ AckedSubtag = alarm + ".Acked",
+ AckCommentSubtag = alarm + ".AckMsg",
+ PrioritySubtag = alarm + ".Priority",
+ };
+ List watchList = new List { target };
+
+ MxAccessComObjectFactory factory = new MxAccessComObjectFactory();
+ LmxSubtagAlarmSource source = new LmxSubtagAlarmSource(factory, clientName: null);
+ using SubtagAlarmConsumer consumer = new SubtagAlarmConsumer(source, watchList);
+
+ ConcurrentQueue transitions =
+ new ConcurrentQueue();
+ consumer.AlarmTransitionEmitted += (_, e) =>
+ {
+ Log("Transition: " + DescribeTransition(e));
+ transitions.Enqueue(e);
+ };
+ source.ValueChanged += (_, c) =>
+ Log(string.Format("raw: {0} = {1}", c.ItemAddress, c.Value ?? ""));
+
+ consumer.Subscribe(string.Format(@"\\{0}\Galaxy!{1}", Environment.MachineName, Group));
+
+ // Warm up so the items go active; drain any initial transition.
+ DateTime warm = DateTime.UtcNow + TimeSpan.FromSeconds(8);
+ while (DateTime.UtcNow < warm) { PumpMessages(); Thread.Sleep(100); }
+ MxAlarmTransitionEvent? ignored;
+ while (transitions.TryDequeue(out ignored)) { }
+
+ Log(">>> WATCHING. Toggle TestMachine_001.TestAlarm002 TRUE, hold ~10s, then FALSE. <<<");
+
+ MxAlarmTransitionEvent? raise = WaitForTransitionKind(
+ transitions, AlarmTransitionKind.Raise, TimeSpan.FromSeconds(150), "Raise");
+ Assert.NotNull(raise);
+ Assert.True(raise!.Record.Degraded, "Subtag-synthesized records must have Degraded=true.");
+ Assert.NotEqual(Guid.Empty, raise.Record.AlarmGuid);
+ Assert.Equal(MxAlarmStateKind.UnackAlm, raise.Record.State);
+ Log(string.Format("RAISE captured: Guid={0} State={1}", raise.Record.AlarmGuid, raise.Record.State));
+
+ MxAlarmTransitionEvent? clear = WaitForTransitionKind(
+ transitions, AlarmTransitionKind.Clear, TimeSpan.FromSeconds(150), "Clear");
+ Assert.NotNull(clear);
+ Log(string.Format("CLEAR captured: State={0}", clear!.Record.State));
+ Log("Operator-driven Raise -> Clear confirmed end-to-end via the subtag route.");
+ }
+
private void RunSmoke()
{
Log(string.Format("AlarmFullReference: {0}", AlarmFullReference));