diff --git a/docs/ScriptedAlarms.md b/docs/ScriptedAlarms.md index 00c18794..f437e58a 100644 --- a/docs/ScriptedAlarms.md +++ b/docs/ScriptedAlarms.md @@ -145,8 +145,9 @@ runtime. #### Severity mapping The authored `severity` (1–1000) seeds the OPC UA condition node at materialisation -time. On the **first native transition** the value snaps to one of four fixed buckets -via `NativeAlarmProjector.MapSeverity`: +time. Every native transition maps the driver's `AlarmSeverity` to one of four fixed +buckets via `NativeAlarmProjector.MapSeverity`, overriding the authored `severity` seed +from the first transition onward: | `AlarmSeverity` enum | Projected value | |---|---| @@ -168,8 +169,8 @@ by `OtOpcUaNodeManager.MapSeverity` on each `AlarmStateUpdate` write: | ≥ 800 | `High` | Consequently the four authored buckets land as: Low→`MediumLow`, Medium→`Medium`, -High→`MediumHigh`, Critical→`High`. The authored `severity` field has no effect after -the first transition. +High→`MediumHigh`, Critical→`High`. The authored `severity` field is overridden by +live driver events on every transition. ### Runtime flow diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DriverInstanceActorNativeAlarmTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DriverInstanceActorNativeAlarmTests.cs index adb0e8dd..b9d2dfcf 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DriverInstanceActorNativeAlarmTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DriverInstanceActorNativeAlarmTests.cs @@ -1,4 +1,5 @@ using Akka.Actor; +using Akka.Event; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Commons.OpcUa; @@ -94,8 +95,10 @@ public sealed class DriverInstanceActorNativeAlarmTests : RuntimeActorTestBase /// /// A native alarm transition that races in while the actor is in Reconnecting is silently /// dropped (debug log only) — it NEVER reaches the parent as - /// and does NOT dead-letter. The feed - /// re-delivers active alarms once the actor re-enters Connected, so dropping here is safe. + /// AND is NOT dead-lettered (the + /// Reconnecting state's explicit Receive<NativeAlarmRaised> handler consumes and + /// discards it, so it never becomes unhandled). The feed re-delivers active alarms once the actor + /// re-enters Connected, so dropping here is safe. /// ( line ~345: the Reconnecting state's /// Receive<NativeAlarmRaised> logs a debug message and discards.) /// @@ -107,10 +110,27 @@ public sealed class DriverInstanceActorNativeAlarmTests : RuntimeActorTestBase /// (detaches handler), then processes the queued NativeAlarmRaised in Reconnecting /// → drops it. The default 10 s reconnect interval ensures no retry fires during the check. /// + /// + /// + /// Both properties are actively asserted: (a) the parent probe receives no + /// ; (b) a dead-letter probe subscribed + /// to on the also receives + /// nothing — proving the drop handler is present (removing it would cause a dead-letter and fail + /// this assertion). NativeAlarmRaised is private to the actor, so the dead-letter probe + /// subscribes to the unfiltered channel; this is safe because + /// exactly one message is injected and the reconnect timer (10 s) cannot fire in the window. + /// /// [Fact] public void Native_alarm_during_reconnect_is_dropped_not_forwarded() { + // Subscribe a dead-letter probe BEFORE injecting the alarm so we don't miss any early publish. + // NativeAlarmRaised is private, so we subscribe to the unfiltered AllDeadLetters channel. + // Only one message is injected and the 10 s reconnect timer can't fire in this window, so + // a plain "no dead letters at all" assertion is safe and non-flaky. + var deadLetters = CreateTestProbe(); + Sys.EventStream.Subscribe(deadLetters.Ref, typeof(AllDeadLetters)); + // Long reconnect interval (default 10 s) so the retry doesn't fire during the assertion window. var driver = new AlarmSourceStubDriver(); var parent = CreateTestProbe(); @@ -134,13 +154,18 @@ public sealed class DriverInstanceActorNativeAlarmTests : RuntimeActorTestBase SourceTimestampUtc: DateTime.UtcNow, Kind: AlarmTransitionKind.Raise)); + // (a) The parent must NOT receive AttributeAlarmPublished from that alarm. // The actor processes: (1) ForceReconnect → Reconnecting (handler detached); // (2) NativeAlarmRaised → dropped (debug log, no forward). - // The parent must NOT receive AttributeAlarmPublished from that alarm. // Wait generously — the default reconnect interval of 10 s means no retry fires here. parent.ExpectNoMsg(TimeSpan.FromMilliseconds(500)); - // The actor must still be alive — Watch + no Terminated (not crashed or dead-lettered). + // (b) The alarm must also NOT have dead-lettered — the Reconnecting state's explicit + // Receive handler must have consumed it. If that handler were removed the + // message would become unhandled → AllDeadLetters, and this assertion would catch the regression. + deadLetters.ExpectNoMsg(TimeSpan.FromMilliseconds(200)); + + // The actor must still be alive — Watch + no Terminated. Watch(actor); parent.ExpectNoMsg(TimeSpan.FromMilliseconds(100)); }