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));