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 19346a4..74ab20f 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker.Tests/Probes/AlarmSubtagLiveSmokeTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker.Tests/Probes/AlarmSubtagLiveSmokeTests.cs
@@ -335,6 +335,97 @@ public sealed class AlarmSubtagLiveSmokeTests
}
}
+ ///
+ /// Discovery run: scans candidate alarm attributes for one whose value
+ /// actually responds to an external write (so it can drive a full
+ /// Raise/Clear lifecycle). Logs each candidate's before/after value and
+ /// InAlarm; does not assert. Runs only with
+ /// MXGATEWAY_RUN_LIVE_MXACCESS_TESTS=1. Side effect: flips any
+ /// writable candidate.
+ ///
+ [LiveMxAccessFact]
+ public void LiveProbe_FindDrivableAlarm()
+ {
+ Exception? threadException = null;
+ ManualResetEventSlim done = new ManualResetEventSlim(false);
+ Thread thread = new Thread(() =>
+ {
+ try { RunFindDrivable(); }
+ 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(), "alarm-find-drivable.log");
+ try { System.IO.File.WriteAllLines(logPath, lines); } catch { /* best effort */ }
+ if (threadException != null) { throw threadException; }
+ }
+
+ private void RunFindDrivable()
+ {
+ string[] candidates = new string[]
+ {
+ "TestMachine_001.TestAlarm002",
+ "TestMachine_001.TestAlarm003",
+ "TestMachine_002.TestAlarm001",
+ "TestMachine_003.TestAlarm001",
+ };
+
+ Log("=== Find a drivable alarm ===");
+ MxAccessComObjectFactory factory = new MxAccessComObjectFactory();
+ using LmxSubtagAlarmSource source = new LmxSubtagAlarmSource(factory, clientName: null);
+
+ Dictionary latest =
+ new Dictionary(StringComparer.OrdinalIgnoreCase);
+ source.ValueChanged += (_, c) =>
+ {
+ latest[c.ItemAddress] = c.Value;
+ Log(string.Format("raw: {0} = {1}", c.ItemAddress, c.Value ?? ""));
+ };
+
+ List toAdvise = new List();
+ foreach (string cand in candidates)
+ {
+ toAdvise.Add(cand);
+ toAdvise.Add(cand + ".InAlarm");
+ }
+ source.Advise(toAdvise);
+ Log("Advised candidates. Pumping ...");
+ PumpFor(TimeSpan.FromSeconds(7));
+
+ foreach (string cand in candidates)
+ {
+ object? before;
+ latest.TryGetValue(cand, out before);
+ bool beforeBool = before is bool bb && bb;
+ Log(string.Format("[{0}] before: value={1} InAlarm={2}",
+ cand, before ?? "", latest.ContainsKey(cand + ".InAlarm") ? latest[cand + ".InAlarm"] : ""));
+
+ Log(string.Format("[{0}] writing {1} ...", cand, !beforeBool));
+ try { source.Write(cand, !beforeBool); }
+ catch (Exception ex) { Log(string.Format("[{0}] write threw: {1}", cand, ex.Message)); continue; }
+
+ PumpFor(TimeSpan.FromSeconds(5));
+ object? after;
+ latest.TryGetValue(cand, out after);
+ bool changed = !Equals(before, after);
+ Log(string.Format("[{0}] after: value={1} InAlarm={2} -> {3}",
+ cand, after ?? "",
+ latest.ContainsKey(cand + ".InAlarm") ? latest[cand + ".InAlarm"] : "",
+ changed ? "DRIVABLE (value changed)" : "not drivable (value held)"));
+ }
+
+ Log("=== Find-drivable scan complete ===");
+ }
+
private void RunSelfDriven()
{
// The discrete alarm's source attribute. Writing its boolean value