using Akka.Actor; using Akka.Event; using Microsoft.EntityFrameworkCore; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Commons.OpcUa; using ZB.MOM.WW.OtOpcUa.Commons.Types; using ZB.MOM.WW.OtOpcUa.Configuration; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; using ZB.MOM.WW.OtOpcUa.Runtime.Drivers; using ZB.MOM.WW.OtOpcUa.Runtime.Tests.Harness; namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Drivers; /// /// Review 02 H5: the Stale behaviour registered no receive for a driver child's value/alarm bursts (nor /// for a late ArtifactForApply / ApplyWedged pipe-back), so any that landed there hit /// Unhandled — dead-letter noise and a dropped message with no log naming why. They are now explicit /// drops. The two private pipe-back records cannot be constructed from a test; the two public child /// messages are pinned here, and the private pair by the same registration pattern (compile-time). /// public sealed class DriverHostActorStaleDropTests : RuntimeActorTestBase { private static readonly NodeId TestNode = NodeId.Parse("stale-drop-test"); [Fact] public void Child_value_and_alarm_bursts_landing_in_Stale_are_dropped_not_unhandled() { var unhandled = CreateTestProbe(); Sys.EventStream.Subscribe(unhandled.Ref, typeof(UnhandledMessage)); // ThrowingDbFactory ⇒ Bootstrap's catch ⇒ Become(Stale). var host = Sys.ActorOf(DriverHostActor.Props( new ThrowingDbFactory(), TestNode, CreateTestProbe().Ref, localRoles: new HashSet { "driver" })); // Barrier: Stale answers RouteNodeWrite synchronously, so a reply proves the host is in Stale. var asker = CreateTestProbe(); host.Tell(new DriverHostActor.RouteNodeWrite("eq-x/speed", 0.0, AddressSpaceRealm.Uns), asker.Ref); asker.ExpectMsg(PresenceBudget).Reason!.ShouldContain("stale"); // Positive control: an unknown message IS reported as unhandled by this host. host.Tell("not a message the host knows"); unhandled.ExpectMsg(m => ReferenceEquals(m.Recipient, host) || m.Recipient.Equals(host), PresenceBudget); host.Tell(new DriverInstanceActor.AttributeValueBatchPublished( "DI-1", new[] { new DriverInstanceActor.AttributeValueItem("Plant/x/y", 1.0, OpcUaStatus.Good, DateTime.UtcNow) })); host.Tell(new DriverInstanceActor.AttributeAlarmPublished( "DI-1", new AlarmEventArgs(new Handle(), "Plant/x/alarm", "Plant/x/alarm", "AlarmCondition", "test", AlarmSeverity.High, DateTime.UtcNow))); // Barrier again, then the absence window. host.Tell(new DriverHostActor.RouteNodeWrite("eq-x/speed", 0.0, AddressSpaceRealm.Uns), asker.Ref); asker.ExpectMsg(PresenceBudget); unhandled.ExpectNoMsg(TimeSpan.FromMilliseconds(500)); } private sealed class Handle : IAlarmSubscriptionHandle { public string DiagnosticId => "stale-test"; } private sealed class ThrowingDbFactory : IDbContextFactory { public OtOpcUaConfigDbContext CreateDbContext() => throw new InvalidOperationException("db unreachable"); } }