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:
@@ -72,10 +72,10 @@ public sealed class FailoverAlarmConsumerTests
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public int AcknowledgeByGuid(Guid g, string c, string a, string b, string d, string e) => 11;
|
||||
public AlarmAckTarget ResolveAckTarget(Guid g) => AlarmAckTarget.For("primary.guid.AckMsg");
|
||||
|
||||
/// <inheritdoc />
|
||||
public int AcknowledgeByName(string n, string p, string gr, string c, string a, string b, string d, string e) => 11;
|
||||
public AlarmAckTarget ResolveAckTargetByName(string n, string p, string gr) => AlarmAckTarget.For("primary.name.AckMsg");
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<MxAlarmSnapshotRecord> SnapshotActiveAlarms(out bool truncated)
|
||||
@@ -122,10 +122,10 @@ public sealed class FailoverAlarmConsumerTests
|
||||
public void PollOnce() { }
|
||||
|
||||
/// <inheritdoc />
|
||||
public int AcknowledgeByGuid(Guid g, string c, string a, string b, string d, string e) => 22;
|
||||
public AlarmAckTarget ResolveAckTarget(Guid g) => AlarmAckTarget.For("standby.guid.AckMsg");
|
||||
|
||||
/// <inheritdoc />
|
||||
public int AcknowledgeByName(string n, string p, string gr, string c, string a, string b, string d, string e) => 22;
|
||||
public AlarmAckTarget ResolveAckTargetByName(string n, string p, string gr) => AlarmAckTarget.For("standby.name.AckMsg");
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<MxAlarmSnapshotRecord> SnapshotActiveAlarms(out bool truncated)
|
||||
@@ -305,9 +305,9 @@ public sealed class FailoverAlarmConsumerTests
|
||||
Assert.Equal(subscribeCountAfterSetup, primary.SubscribeCount);
|
||||
}
|
||||
|
||||
/// <summary>Proves that acknowledgment calls delegate to whichever child (primary or standby) is currently active.</summary>
|
||||
/// <summary>Proves that ack-target resolution delegates to whichever child (primary or standby) is currently active.</summary>
|
||||
[Fact]
|
||||
public void Acknowledge_DelegatesToActiveChild()
|
||||
public void ResolveAckTarget_DelegatesToActiveChild()
|
||||
{
|
||||
FlakyPrimary primary = new FlakyPrimary { ThrowOnPoll = false };
|
||||
StubStandby standby = new StubStandby();
|
||||
@@ -316,18 +316,18 @@ public sealed class FailoverAlarmConsumerTests
|
||||
|
||||
sut.Subscribe(@"\\HOST\Galaxy!Area");
|
||||
|
||||
// Active = Primary → primary's sentinel value (11).
|
||||
Assert.Equal(11, sut.AcknowledgeByGuid(Guid.NewGuid(), "c", "n", "node", "dom", "full"));
|
||||
Assert.Equal(11, sut.AcknowledgeByName("a", "p", "g", "c", "n", "node", "dom", "full"));
|
||||
// Active = Primary → primary's sentinel targets.
|
||||
Assert.Equal("primary.guid.AckMsg", sut.ResolveAckTarget(Guid.NewGuid()).ItemAddress);
|
||||
Assert.Equal("primary.name.AckMsg", sut.ResolveAckTargetByName("a", "p", "g").ItemAddress);
|
||||
|
||||
// Force a failover by failing the primary past threshold.
|
||||
primary.ThrowOnPoll = true;
|
||||
sut.PollOnce(); // threshold=1 → switch to Standby
|
||||
Assert.Equal(AlarmProviderMode.Subtag, sut.Mode);
|
||||
|
||||
// Active = Standby → standby's sentinel value (22).
|
||||
Assert.Equal(22, sut.AcknowledgeByGuid(Guid.NewGuid(), "c", "n", "node", "dom", "full"));
|
||||
Assert.Equal(22, sut.AcknowledgeByName("a", "p", "g", "c", "n", "node", "dom", "full"));
|
||||
// Active = Standby → standby's sentinel targets.
|
||||
Assert.Equal("standby.guid.AckMsg", sut.ResolveAckTarget(Guid.NewGuid()).ItemAddress);
|
||||
Assert.Equal("standby.name.AckMsg", sut.ResolveAckTargetByName("a", "p", "g").ItemAddress);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -94,76 +94,28 @@ public sealed class LmxSubtagAlarmSourceTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies <see cref="LmxSubtagAlarmSource.Write"/> adds the item when
|
||||
/// it was not previously advised and writes with user id 0.
|
||||
/// Verifies <see cref="LmxSubtagAlarmSource.Dispose"/> UnAdvises and
|
||||
/// removes every advised handle and unregisters the server exactly once.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Write_AddsItemWhenUnknownAndWrites()
|
||||
{
|
||||
var server = new RecordingMxAccessServer();
|
||||
using var source = new LmxSubtagAlarmSource(server, FakeServerHandle);
|
||||
|
||||
source.Write("Tank1.Alarm.AckComment", "acknowledged");
|
||||
|
||||
Assert.Contains("Tank1.Alarm.AckComment", server.AddedItems);
|
||||
Assert.Single(server.Writes);
|
||||
RecordingMxAccessServer.WriteRecord write = server.Writes[0];
|
||||
Assert.Equal(FakeServerHandle, write.ServerHandle);
|
||||
Assert.Equal("acknowledged", write.Value);
|
||||
Assert.Equal(0, write.UserId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies <see cref="LmxSubtagAlarmSource.Write"/> reuses an existing
|
||||
/// item handle (no duplicate AddItem) when the address was already
|
||||
/// advised.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Write_ReusesHandleForAdvisedAddress()
|
||||
{
|
||||
var server = new RecordingMxAccessServer();
|
||||
using var source = new LmxSubtagAlarmSource(server, FakeServerHandle);
|
||||
|
||||
source.Advise(new[] { "Tank1.Alarm.Subtag" });
|
||||
int adviseAddCount = server.AddedItems.Count;
|
||||
|
||||
source.Write("Tank1.Alarm.Subtag", true);
|
||||
|
||||
// No second AddItem for the already-advised address.
|
||||
Assert.Equal(adviseAddCount, server.AddedItems.Count);
|
||||
Assert.Single(server.Writes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies <see cref="LmxSubtagAlarmSource.Dispose"/> UnAdvises only the
|
||||
/// handles that were actually advised — a write-only item (added by
|
||||
/// <see cref="LmxSubtagAlarmSource.Write"/> but never advised) is removed
|
||||
/// but not unadvised — and unregisters the server exactly once.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Dispose_UnAdvisesOnlyAdvisedHandles_RemovesAll_AndUnregistersOnce()
|
||||
public void Dispose_UnAdvisesAndRemovesAdvisedHandles_AndUnregistersOnce()
|
||||
{
|
||||
var server = new RecordingMxAccessServer();
|
||||
var source = new LmxSubtagAlarmSource(server, FakeServerHandle);
|
||||
|
||||
source.Advise(new[] { "Tank1.Alarm.Subtag", "Tank2.Alarm.Subtag" });
|
||||
// A write-only subtag: added by Write, never advised.
|
||||
source.Write("Tank1.Alarm.AckComment", "acknowledged");
|
||||
|
||||
int advised1 = server.LastItemHandleFor("Tank1.Alarm.Subtag");
|
||||
int advised2 = server.LastItemHandleFor("Tank2.Alarm.Subtag");
|
||||
int writeOnly = server.LastItemHandleFor("Tank1.Alarm.AckComment");
|
||||
|
||||
source.Dispose();
|
||||
|
||||
// Only the two advised handles are unadvised — never the write-only one.
|
||||
Assert.Equal(new[] { advised1, advised2 }, server.UnAdvisedItemHandles);
|
||||
Assert.DoesNotContain(writeOnly, server.UnAdvisedItemHandles);
|
||||
// Every added item (advised + write-only) is removed.
|
||||
Assert.Equal(
|
||||
new[] { advised1, advised2, writeOnly }.OrderBy(h => h),
|
||||
new[] { advised1, advised2 }.OrderBy(h => h),
|
||||
server.RemovedItemHandles.OrderBy(h => h));
|
||||
Assert.Equal(1, server.UnregisterCount);
|
||||
Assert.Empty(server.Writes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace ZB.MOM.WW.MxGateway.Worker.Tests.MxAccess;
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="SubtagAlarmConsumer"/>: prove that the subtag
|
||||
/// fallback advises the observable subtags, synthesizes degraded
|
||||
/// transitions with stable synthetic GUIDs, routes acknowledgments to the
|
||||
/// transitions with stable synthetic GUIDs, resolves acknowledgments to the
|
||||
/// ack-comment subtag, and stamps snapshots. A <see cref="FakeSource"/>
|
||||
/// stands in for the live MXAccess subtag source so this needs no AVEVA
|
||||
/// install.
|
||||
@@ -40,9 +40,13 @@ public sealed class SubtagAlarmConsumerTests
|
||||
return new SubtagAlarmConsumer(source, new[] { BuildTarget() });
|
||||
}
|
||||
|
||||
/// <summary>Verifies Subscribe advises the active, acked, priority, and ack-comment subtags (the ack-comment is advised so it is an active, writable MXAccess item).</summary>
|
||||
/// <summary>
|
||||
/// Verifies Subscribe advises the observable active, acked and priority
|
||||
/// subtags but not the ack-comment subtag: the ack is written by
|
||||
/// <see cref="AlarmAckWriter"/>, which adds and advises it on its own server.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Subscribe_AdvisesAllSubtagsIncludingAckComment()
|
||||
public void Subscribe_AdvisesObservableSubtagsButNotAckComment()
|
||||
{
|
||||
FakeSource source = new FakeSource();
|
||||
using SubtagAlarmConsumer consumer = BuildConsumer(source);
|
||||
@@ -52,7 +56,7 @@ public sealed class SubtagAlarmConsumerTests
|
||||
Assert.Contains(ActiveSubtag, source.Advised);
|
||||
Assert.Contains(AckedSubtag, source.Advised);
|
||||
Assert.Contains(PrioritySubtag, source.Advised);
|
||||
Assert.Contains(AckCommentSubtag, source.Advised);
|
||||
Assert.DoesNotContain(AckCommentSubtag, source.Advised);
|
||||
}
|
||||
|
||||
/// <summary>Verifies an active=true value change raises a degraded, GUID-stamped UNACK_ALM transition.</summary>
|
||||
@@ -74,50 +78,49 @@ public sealed class SubtagAlarmConsumerTests
|
||||
Assert.NotEqual(Guid.Empty, emitted.Record.AlarmGuid);
|
||||
}
|
||||
|
||||
/// <summary>Verifies AcknowledgeByName writes the comment to the ack-comment subtag and returns success.</summary>
|
||||
/// <summary>Verifies ResolveAckTargetByName resolves to the target's ack-comment subtag.</summary>
|
||||
[Fact]
|
||||
public void AcknowledgeByName_WritesCommentToAckCommentSubtag()
|
||||
public void ResolveAckTargetByName_ReturnsAckCommentSubtag()
|
||||
{
|
||||
FakeSource source = new FakeSource();
|
||||
using SubtagAlarmConsumer consumer = BuildConsumer(source);
|
||||
consumer.Subscribe(@"\\HOST\Galaxy!TestArea");
|
||||
|
||||
int rc = consumer.AcknowledgeByName(
|
||||
alarmName: "Tank01.Level.HiHi",
|
||||
providerName: "Galaxy",
|
||||
groupName: "TestArea",
|
||||
ackComment: "operator ack",
|
||||
ackOperatorName: "alice",
|
||||
ackOperatorNode: "WS01",
|
||||
ackOperatorDomain: "CORP",
|
||||
ackOperatorFullName: "Alice Smith");
|
||||
AlarmAckTarget target = consumer.ResolveAckTargetByName("Tank01.Level.HiHi", "Galaxy", "TestArea");
|
||||
|
||||
Assert.Equal(0, rc);
|
||||
Assert.NotNull(source.LastWrite);
|
||||
Assert.Equal(AckCommentSubtag, source.LastWrite!.Value.Address);
|
||||
Assert.Equal("operator ack", source.LastWrite!.Value.Value);
|
||||
Assert.True(target.Found);
|
||||
Assert.Equal(AckCommentSubtag, target.ItemAddress);
|
||||
}
|
||||
|
||||
/// <summary>Verifies AcknowledgeByName returns non-zero when no target matches the supplied name.</summary>
|
||||
/// <summary>Verifies ResolveAckTargetByName reports AlarmNotFound when no target matches the supplied name.</summary>
|
||||
[Fact]
|
||||
public void AcknowledgeByName_UnknownAlarm_ReturnsNonZero()
|
||||
public void ResolveAckTargetByName_UnknownAlarm_IsNotFound()
|
||||
{
|
||||
FakeSource source = new FakeSource();
|
||||
using SubtagAlarmConsumer consumer = BuildConsumer(source);
|
||||
consumer.Subscribe(@"\\HOST\Galaxy!TestArea");
|
||||
|
||||
int rc = consumer.AcknowledgeByName(
|
||||
alarmName: "DoesNotExist.NoSuchAlarm",
|
||||
providerName: "Galaxy",
|
||||
groupName: "TestArea",
|
||||
ackComment: "operator ack",
|
||||
ackOperatorName: "alice",
|
||||
ackOperatorNode: "WS01",
|
||||
ackOperatorDomain: "CORP",
|
||||
ackOperatorFullName: "Alice Smith");
|
||||
AlarmAckTarget target = consumer.ResolveAckTargetByName("DoesNotExist.NoSuchAlarm", "Galaxy", "TestArea");
|
||||
|
||||
Assert.NotEqual(0, rc);
|
||||
Assert.Null(source.LastWrite);
|
||||
Assert.False(target.Found);
|
||||
Assert.Equal(AlarmAckOutcome.AlarmNotFound, target.Failure);
|
||||
}
|
||||
|
||||
/// <summary>Verifies a target with no ack-comment subtag resolves as NotAckable, by name and by GUID.</summary>
|
||||
[Fact]
|
||||
public void ResolveAckTarget_EmptyAckCommentSubtag_IsNotAckable()
|
||||
{
|
||||
AlarmSubtagTarget noAck = BuildTarget();
|
||||
noAck.AckCommentSubtag = string.Empty;
|
||||
FakeSource source = new FakeSource();
|
||||
using SubtagAlarmConsumer consumer = new SubtagAlarmConsumer(source, new[] { noAck });
|
||||
|
||||
Assert.Equal(
|
||||
AlarmAckOutcome.NotAckable,
|
||||
consumer.ResolveAckTargetByName("Tank01.Level.HiHi", "Galaxy", "TestArea").Failure);
|
||||
Assert.Equal(
|
||||
AlarmAckOutcome.NotAckable,
|
||||
consumer.ResolveAckTarget(SyntheticAlarmGuid.ForReference(Reference)).Failure);
|
||||
}
|
||||
|
||||
/// <summary>Verifies a snapshot of an active alarm stamps Degraded and a non-empty synthetic GUID.</summary>
|
||||
@@ -160,11 +163,11 @@ public sealed class SubtagAlarmConsumerTests
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that when two alarm targets share a prefix (e.g. Level.Hi vs Level.HiHi),
|
||||
/// AcknowledgeByName routes each ack to its own ack-comment subtag and never
|
||||
/// ResolveAckTargetByName resolves each to its own ack-comment subtag and never
|
||||
/// conflates the shorter name with the longer one.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AcknowledgeByName_PrefixNameDoesNotFalseMatch()
|
||||
public void ResolveAckTargetByName_PrefixNameDoesNotFalseMatch()
|
||||
{
|
||||
const string ReferenceHi = "Galaxy!Area.Tank01.Level.Hi";
|
||||
const string ReferenceHiHi = "Galaxy!Area.Tank01.Level.HiHi";
|
||||
@@ -195,42 +198,19 @@ public sealed class SubtagAlarmConsumerTests
|
||||
source, new[] { targetHi, targetHiHi });
|
||||
consumer.Subscribe(@"\\HOST\Galaxy!Area");
|
||||
|
||||
// Ack the shorter name — must write to the shorter target's subtag only.
|
||||
int rcHi = consumer.AcknowledgeByName(
|
||||
alarmName: "Tank01.Level.Hi",
|
||||
providerName: "Galaxy",
|
||||
groupName: "Area",
|
||||
ackComment: "ack hi",
|
||||
ackOperatorName: "op", ackOperatorNode: "WS01",
|
||||
ackOperatorDomain: "CORP", ackOperatorFullName: "Operator");
|
||||
|
||||
Assert.Equal(0, rcHi);
|
||||
Assert.NotNull(source.LastWrite);
|
||||
Assert.Equal(AckCommentHi, source.LastWrite!.Value.Address);
|
||||
Assert.Equal("ack hi", source.LastWrite.Value.Value);
|
||||
|
||||
// Ack the longer name — must write to the longer target's subtag.
|
||||
int rcHiHi = consumer.AcknowledgeByName(
|
||||
alarmName: "Tank01.Level.HiHi",
|
||||
providerName: "Galaxy",
|
||||
groupName: "Area",
|
||||
ackComment: "ack hihi",
|
||||
ackOperatorName: "op", ackOperatorNode: "WS01",
|
||||
ackOperatorDomain: "CORP", ackOperatorFullName: "Operator");
|
||||
|
||||
Assert.Equal(0, rcHiHi);
|
||||
Assert.NotNull(source.LastWrite);
|
||||
Assert.Equal(AckCommentHiHi, source.LastWrite!.Value.Address);
|
||||
Assert.Equal("ack hihi", source.LastWrite.Value.Value);
|
||||
Assert.Equal(AckCommentHi, consumer.ResolveAckTargetByName("Tank01.Level.Hi", "Galaxy", "Area").ItemAddress);
|
||||
Assert.Equal(AckCommentHiHi, consumer.ResolveAckTargetByName("Tank01.Level.HiHi", "Galaxy", "Area").ItemAddress);
|
||||
Assert.Equal(
|
||||
AlarmAckOutcome.AlarmNotFound,
|
||||
consumer.ResolveAckTargetByName("Tank01.Level.H", "Galaxy", "Area").Failure);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies AcknowledgeByGuid resolves the synthetic GUID (computed from
|
||||
/// the alarm's full reference) to the correct target and writes the comment
|
||||
/// to that target's ack-comment subtag.
|
||||
/// Verifies ResolveAckTarget resolves the synthetic GUID (computed from
|
||||
/// the alarm's full reference) to that target's ack-comment subtag.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AcknowledgeByGuid_WritesCommentToAckCommentSubtag()
|
||||
public void ResolveAckTarget_SyntheticGuid_ReturnsAckCommentSubtag()
|
||||
{
|
||||
FakeSource source = new FakeSource();
|
||||
using SubtagAlarmConsumer consumer = BuildConsumer(source);
|
||||
@@ -246,41 +226,21 @@ public sealed class SubtagAlarmConsumerTests
|
||||
Guid syntheticGuid = emitted!.Record.AlarmGuid;
|
||||
Assert.NotEqual(Guid.Empty, syntheticGuid);
|
||||
|
||||
int rc = consumer.AcknowledgeByGuid(
|
||||
alarmGuid: syntheticGuid,
|
||||
ackComment: "guid ack",
|
||||
ackOperatorName: "op",
|
||||
ackOperatorNode: "WS01",
|
||||
ackOperatorDomain: "CORP",
|
||||
ackOperatorFullName: "Operator");
|
||||
AlarmAckTarget target = consumer.ResolveAckTarget(syntheticGuid);
|
||||
|
||||
Assert.Equal(0, rc);
|
||||
Assert.NotNull(source.LastWrite);
|
||||
Assert.Equal(AckCommentSubtag, source.LastWrite!.Value.Address);
|
||||
Assert.Equal("guid ack", source.LastWrite.Value.Value);
|
||||
Assert.True(target.Found);
|
||||
Assert.Equal(AckCommentSubtag, target.ItemAddress);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies AcknowledgeByGuid returns non-zero and performs no write when
|
||||
/// the supplied GUID is not known to the consumer.
|
||||
/// </summary>
|
||||
/// <summary>Verifies ResolveAckTarget reports AlarmNotFound for a GUID unknown to the consumer.</summary>
|
||||
[Fact]
|
||||
public void AcknowledgeByGuid_UnknownGuid_ReturnsNonZero()
|
||||
public void ResolveAckTarget_UnknownGuid_IsNotFound()
|
||||
{
|
||||
FakeSource source = new FakeSource();
|
||||
using SubtagAlarmConsumer consumer = BuildConsumer(source);
|
||||
consumer.Subscribe(@"\\HOST\Galaxy!TestArea");
|
||||
|
||||
int rc = consumer.AcknowledgeByGuid(
|
||||
alarmGuid: Guid.NewGuid(),
|
||||
ackComment: "should not write",
|
||||
ackOperatorName: "op",
|
||||
ackOperatorNode: "WS01",
|
||||
ackOperatorDomain: "CORP",
|
||||
ackOperatorFullName: "Operator");
|
||||
|
||||
Assert.NotEqual(0, rc);
|
||||
Assert.Null(source.LastWrite);
|
||||
Assert.Equal(AlarmAckOutcome.AlarmNotFound, consumer.ResolveAckTarget(Guid.NewGuid()).Failure);
|
||||
}
|
||||
|
||||
private sealed class FakeSource : ISubtagAlarmSource
|
||||
@@ -291,21 +251,12 @@ public sealed class SubtagAlarmConsumerTests
|
||||
/// <summary>Gets the subtag addresses passed to <see cref="Advise"/>.</summary>
|
||||
public List<string> Advised { get; } = new List<string>();
|
||||
|
||||
/// <summary>Gets the most recent (address, value) pair passed to <see cref="Write"/>.</summary>
|
||||
public (string Address, object? Value)? LastWrite { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Advise(IReadOnlyCollection<string> itemAddresses)
|
||||
{
|
||||
Advised.AddRange(itemAddresses);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Write(string itemAddress, object? value)
|
||||
{
|
||||
LastWrite = (itemAddress, value);
|
||||
}
|
||||
|
||||
/// <summary>Raises a <see cref="SubtagValueChange"/> for the given subtag.</summary>
|
||||
/// <param name="address">The subtag address whose value changed.</param>
|
||||
/// <param name="value">The new value.</param>
|
||||
|
||||
@@ -824,4 +824,58 @@ public sealed class WnWrapAlarmConsumerXmlTests
|
||||
Assert.Equal("mgack-authwrite 180714", record.AckComment);
|
||||
Assert.Equal("Test alarm #1", record.AlarmComment);
|
||||
}
|
||||
|
||||
/// <summary>A Galaxy-provider record resolves to its <c><tag>.AckMsg</c> item; an unknown GUID is not found.</summary>
|
||||
[Fact]
|
||||
public void ResolveAckTarget_GalaxyRecord_ReturnsAckMsgItem()
|
||||
{
|
||||
Guid guid = Guid.NewGuid();
|
||||
using WnWrapAlarmConsumer consumer = CreateConsumerWithSnapshot(new MxAlarmSnapshotRecord
|
||||
{
|
||||
AlarmGuid = guid, ProviderName = "Galaxy", Group = "TestArea", TagName = "TestMachine_001.TestAlarm001", State = MxAlarmStateKind.UnackAlm,
|
||||
});
|
||||
|
||||
AlarmAckTarget target = consumer.ResolveAckTarget(guid);
|
||||
|
||||
Assert.True(target.Found);
|
||||
Assert.Equal("TestMachine_001.TestAlarm001.AckMsg", target.ItemAddress);
|
||||
Assert.Equal(AlarmAckOutcome.AlarmNotFound, consumer.ResolveAckTarget(Guid.NewGuid()).Failure);
|
||||
}
|
||||
|
||||
/// <summary>A non-Galaxy provider has no MXAccess ack attribute, so it is not ackable.</summary>
|
||||
[Fact]
|
||||
public void ResolveAckTarget_NonGalaxyProvider_IsNotAckable()
|
||||
{
|
||||
Guid guid = Guid.NewGuid();
|
||||
using WnWrapAlarmConsumer consumer = CreateConsumerWithSnapshot(new MxAlarmSnapshotRecord
|
||||
{
|
||||
AlarmGuid = guid, ProviderName = "InTouch", Group = "G", TagName = "Tag1", State = MxAlarmStateKind.UnackAlm,
|
||||
});
|
||||
|
||||
Assert.Equal(AlarmAckOutcome.NotAckable, consumer.ResolveAckTarget(guid).Failure);
|
||||
}
|
||||
|
||||
/// <summary>By-name resolution needs no snapshot: a Galaxy name maps straight to its AckMsg item.</summary>
|
||||
[Fact]
|
||||
public void ResolveAckTargetByName_Galaxy_ReturnsAckMsgItem()
|
||||
{
|
||||
using WnWrapAlarmConsumer consumer = CreateConsumerWithSnapshot();
|
||||
|
||||
Assert.Equal("TestMachine_001.TestAlarm001.AckMsg",
|
||||
consumer.ResolveAckTargetByName("TestMachine_001.TestAlarm001", "Galaxy", "TestArea").ItemAddress);
|
||||
Assert.Equal(AlarmAckOutcome.NotAckable, consumer.ResolveAckTargetByName("Tag1", "InTouch", "G").Failure);
|
||||
}
|
||||
|
||||
private static WnWrapAlarmConsumer CreateConsumerWithSnapshot(params MxAlarmSnapshotRecord[] records)
|
||||
{
|
||||
WnWrapAlarmConsumer consumer = new WnWrapAlarmConsumer(1024);
|
||||
Dictionary<Guid, MxAlarmSnapshotRecord> next = new Dictionary<Guid, MxAlarmSnapshotRecord>();
|
||||
foreach (MxAlarmSnapshotRecord record in records)
|
||||
{
|
||||
next[record.AlarmGuid] = record;
|
||||
}
|
||||
|
||||
consumer.FoldFetch(next, truncated: false, out _);
|
||||
return consumer;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user