fix(opcuaclient): derive AlarmTransitionKind from upstream ActiveState/AckedState (#619 review)
NativeAlarmProjector derives Active/Acked ONLY from AlarmEventArgs.Kind, and this driver never set it - every routed event projected Unspecified, leaving the condition inactive+acked (only Message/Severity moved). The live-gate expectation 'toggling Active as opc-plc cycles' could not have passed. Select ActiveState/Id + AckedState/Id (two-element browse paths) and emit the Kind sequence that moves the projector from the state last told to the host to the event's (Raise+Acknowledge when a notification carries both). Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
@@ -196,6 +196,42 @@ public sealed class OpcUaClientAlarmTests
|
||||
target.Value.EventId.ShouldBe(new byte[] { 1 });
|
||||
}
|
||||
|
||||
// ---- #619 review: the host's NativeAlarmProjector derives Active/Acked ONLY from AlarmEventArgs.Kind; an
|
||||
// Unspecified kind keeps the prior state (inactive+acked when fresh), so without a Kind no upstream alarm
|
||||
// could ever show active. ----
|
||||
|
||||
/// <summary>ActiveState/Id and AckedState/Id are read from the two trailing select clauses.</summary>
|
||||
[Fact]
|
||||
public void ProjectEvent_reads_the_condition_state()
|
||||
{
|
||||
var index = new Dictionary<string, string> { ["ns=3;s=Alarms.Trip1"] = "Plant/opc/plc/Trip1" };
|
||||
var fields = EventFields("ns=3;s=Alarms.Trip1", "ns=3;s=Alarms.Trip1.Cond");
|
||||
fields.Add(new Variant(true)); // ActiveState/Id
|
||||
fields.Add(new Variant(false)); // AckedState/Id
|
||||
|
||||
OpcUaClientDriver.ProjectEvent(fields, index, new FakeAlarmHandle(), n => n.ToString(), (_, _) => { },
|
||||
out var state);
|
||||
|
||||
state.ShouldBe((true, false));
|
||||
}
|
||||
|
||||
/// <summary>The transition sequence drives a projector from the driver's last-told state to the event's.</summary>
|
||||
[Theory]
|
||||
[InlineData(false, true, true, false, new[] { AlarmTransitionKind.Raise })]
|
||||
[InlineData(false, true, true, true, new[] { AlarmTransitionKind.Raise, AlarmTransitionKind.Acknowledge })]
|
||||
[InlineData(true, false, true, true, new[] { AlarmTransitionKind.Acknowledge })]
|
||||
[InlineData(true, false, false, false, new[] { AlarmTransitionKind.Clear })]
|
||||
[InlineData(true, true, false, true, new[] { AlarmTransitionKind.Clear })]
|
||||
[InlineData(false, false, false, true, new[] { AlarmTransitionKind.Acknowledge })]
|
||||
[InlineData(true, false, false, true, new[] { AlarmTransitionKind.Clear, AlarmTransitionKind.Acknowledge })]
|
||||
[InlineData(true, true, true, false, new[] { AlarmTransitionKind.Retrigger })]
|
||||
[InlineData(true, false, true, false, new[] { AlarmTransitionKind.Unspecified })]
|
||||
public void AlarmTransitions_move_prior_to_target(
|
||||
bool priorActive, bool priorAcked, bool active, bool acked, AlarmTransitionKind[] expected)
|
||||
{
|
||||
OpcUaClientDriver.AlarmTransitions((priorActive, priorAcked), (active, acked)).ShouldBe(expected);
|
||||
}
|
||||
|
||||
private sealed class FakeAlarmHandle : IAlarmSubscriptionHandle
|
||||
{
|
||||
/// <summary>Gets the diagnostic identifier for this alarm handle.</summary>
|
||||
|
||||
Reference in New Issue
Block a user