refactor(worker): alarm consumers resolve the AckMsg item instead of acking

IMxAccessAlarmConsumer.ResolveAckTarget/ResolveAckTargetByName replace AcknowledgeByGuid/AcknowledgeByName. wnwrap maps Galaxy-provider alarms to <tag>.AckMsg and drops its inert parallel ack consumer; subtag resolves to the target's AckCommentSubtag and no longer advises or writes it; ISubtagAlarmSource.Write is removed. Does not build alone: AlarmDispatcher moves to the new API in the next commit.

Claude-Session: https://claude.ai/code/session_016J6isfcgqy4SB2RK3ZvFvT
This commit is contained in:
Joseph Doherty
2026-10-19 19:35:49 -04:00
parent 8199553734
commit 0d4b2aea2d
11 changed files with 238 additions and 452 deletions
@@ -2,7 +2,7 @@
//
// Validates the subtag-fallback pipeline against a real Galaxy + MXAccess install:
// LmxSubtagAlarmSource (own LMXProxyServerClass) ->
// SubtagAlarmConsumer (state machine + AcknowledgeByName write) ->
// SubtagAlarmConsumer (state machine + ResolveAckTargetByName) + AlarmAckWriter (AckMsg write) ->
// synthesized MxAlarmTransitionEvent (Raise / Clear, Degraded=true, SyntheticGuid)
//
// FIELD NAMES CONFIRMED: InAlarm/Acked/AckMsg/Priority are the confirmed AVEVA
@@ -49,8 +49,9 @@ namespace ZB.MOM.WW.MxGateway.Worker.Tests.Probes;
/// synthesize <see cref="MxAlarmTransitionEvent"/> records from Galaxy
/// alarm subtags using the confirmed AVEVA <c>AlarmExtension</c> field
/// names (InAlarm/Acked/AckMsg/Priority), and that
/// <see cref="SubtagAlarmConsumer.AcknowledgeByName"/> writes the
/// ack-comment subtag (AckMsg) successfully.
/// <see cref="SubtagAlarmConsumer.ResolveAckTargetByName"/> resolves the
/// ack-comment subtag (AckMsg) that <see cref="AlarmAckWriter"/> then
/// writes successfully.
///
/// Skip-gated; flip <c>Skip=null</c> on the dev rig with an alarm being
/// driven (flip script or a manual operator/IDE toggle of the alarm
@@ -119,7 +120,7 @@ public sealed class AlarmSubtagLiveSmokeTests
/// real <c>LMXProxyServerClass</c>, collects a Raise then a Clear
/// transition synthesized by <see cref="SubtagAlarmConsumer"/>, confirms
/// the Degraded flag and synthetic GUID are stamped, then
/// AcknowledgeByName and verifies the ack-comment write returns 0.
/// acknowledges through the resolved ack-comment subtag and verifies the write succeeds.
/// </summary>
[Fact(Skip = "Live dev-rig smoke test — flip Skip=null with AVEVA + an alarm being driven (flip script or manual operator/IDE toggle of the alarm attribute). Subtag fallback path. Field names confirmed (InAlarm/Acked/AckMsg/Priority); live-validate runtime path resolves without intermediate alarm-condition segment.")]
public void SubtagFallback_FullPipelineRoundTrip_SynthesizesRaiseAndAcknowledges()
@@ -257,17 +258,9 @@ public sealed class AlarmSubtagLiveSmokeTests
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));
Log("Acknowledge (writes AckMsg subtag via AlarmAckWriter) ...");
int rc = AckViaWriter(consumer, "subtag-resolution-probe").NativeStatus;
Log(string.Format("Ack (AckMsg write) -> rc={0}", rc));
DateTime settle = DateTime.UtcNow + TimeSpan.FromSeconds(2);
while (DateTime.UtcNow < settle)
@@ -392,21 +385,13 @@ public sealed class AlarmSubtagLiveSmokeTests
Assert.True(foundInSnapshot,
string.Format("Active alarm snapshot must contain '{0}' after a Raise.", AlarmFullReference));
// 3. AcknowledgeByName — writes the ack-comment subtag.
// 3. Resolve the ack-comment subtag and write it as the operator.
// The dispatcher derives (alarmName, provider, group) via the same
// TryParseReference logic as AlarmsLiveSmokeTests.
Log("Calling AcknowledgeByName ...");
int rc = consumer.AcknowledgeByName(
alarmName: AlarmTagName,
providerName: Provider,
groupName: Group,
ackComment: "subtag-fallback-smoke ack",
ackOperatorName: Environment.UserName,
ackOperatorNode: Environment.MachineName,
ackOperatorDomain: Environment.UserDomainName ?? string.Empty,
ackOperatorFullName: Environment.UserName);
Log("Acknowledging via AlarmAckWriter ...");
int rc = AckViaWriter(consumer, "subtag-fallback-smoke ack").NativeStatus;
Log(string.Format("AcknowledgeByName -> rc={0}", rc));
Log(string.Format("Ack -> rc={0}", rc));
Assert.Equal(0, rc);
// 4. Wait for a Clear or Acknowledge transition to confirm the state
@@ -501,6 +486,29 @@ public sealed class AlarmSubtagLiveSmokeTests
/// Mirrors the pump pattern documented in
/// <c>docs/MxAccessWorkerInstanceDesign.md</c>.
/// </summary>
/// <summary>
/// Resolves the probe alarm's ack item through the consumer and writes it
/// with <see cref="AlarmAckWriter"/> as the operator named by
/// <c>MXGATEWAY_PROBE_ACK_USER</c>/<c>MXGATEWAY_PROBE_ACK_PASSWORD</c>
/// (default: current user, empty password).
/// </summary>
private static AlarmAckResult AckViaWriter(SubtagAlarmConsumer consumer, string comment)
{
AlarmAckTarget target = consumer.ResolveAckTargetByName(AlarmTagName, Provider, Group);
if (!target.Found)
{
return AlarmAckResult.FromTarget(target);
}
using AlarmAckWriter writer = new AlarmAckWriter(
new MxAccessComObjectFactory(), PumpMessages, TimeSpan.FromSeconds(5), AlarmAckWriter.DefaultClientName);
return writer.Acknowledge(target.ItemAddress, comment, new AlarmAckOperator
{
UserName = Environment.GetEnvironmentVariable("MXGATEWAY_PROBE_ACK_USER") ?? Environment.UserName,
Password = Environment.GetEnvironmentVariable("MXGATEWAY_PROBE_ACK_PASSWORD") ?? string.Empty,
});
}
private static void PumpMessages()
{
NativeMethods.MSG msg;