using Akka.Actor; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Commons.Messages.Mesh; namespace ZB.MOM.WW.OtOpcUa.Host.IntegrationTests; /// /// Pins that both mesh comm actors are actually spawned, under their declared names, on a /// really booted host. /// /// /// /// What this pins is existence, not a wire contract. Since the cross-mesh transport /// became gRPC, both services resolve their comm actor through ActorRegistry rather /// than by path, so a rename is a compile error rather than a silent drop and /// is only a local naming convention. What is still unpinned by /// anything else is whether the two registration sites /// (WithOtOpcUaControlPlaneSingletons / WithOtOpcUaRuntimeActors) actually run /// and spawn these actors on a real node — and if either did not, every inbound call would be /// refused as Unavailable ("the node is still starting"), which reads as a boot-window /// race rather than as an actor that will never appear. /// /// /// Booting the real host is the point. A test that spawned the actors itself would pin the /// constant against itself and prove nothing about that wiring. /// /// public class MeshCommActorPathTests { /// /// Builds the ActorSelection string for a top-level actor name. Composed here rather /// than held as a constant: the full path is a local addressing detail of this test, not /// something the product declares — see . /// /// The declared actor name. /// The absolute user-guardian path. private static string UserPath(string actorName) => "/user/" + actorName; private static async Task AssertActorExistsAsync(ActorSystem system, string actorName) { var path = UserPath(actorName); var identity = await system.ActorSelection(path) .Ask(new Identify(path), TimeSpan.FromSeconds(10)); identity.Subject.ShouldNotBeNull( $"no actor exists at {path}; the registration that spawns it did not run, and every " + "inbound mesh call to this node would be refused as a misleading readiness rejection"); } [Fact] public async Task Both_mesh_comm_actors_are_spawned_under_their_declared_names() { await using var harness = await TwoNodeClusterHarness.StartAsync(); // Harness nodes carry admin+driver, so BOTH comm actors live on each of them. await AssertActorExistsAsync(harness.NodeASystem, MeshPaths.CentralCommunicationName); await AssertActorExistsAsync(harness.NodeASystem, MeshPaths.NodeCommunicationName); await AssertActorExistsAsync(harness.NodeBSystem, MeshPaths.CentralCommunicationName); await AssertActorExistsAsync(harness.NodeBSystem, MeshPaths.NodeCommunicationName); } // NOT TESTED HERE: that the comm actors are registered per node rather than as cluster // singletons. Two candidate discriminators were tried and BOTH were invalid: // // * "the path resolves on both nodes" — a ClusterSingletonManager is also created on every // node in the role, so this passes under either shape. // * "no /singleton child exists" — measured against the KNOWN singleton /user/config-publish, // that child is null there too, so Akka.Hosting does not lay singletons out this way and // the assertion proves nothing. // // A sabotage that re-registered CentralCommunicationActor via WithSingleton left both green, // which is what exposed them. Rather than ship an assertion I could not make fail, the property // is left to live-gate step 8 (stop the OLDEST central; the survivor's comm actor must still // receive a node ack, which a singleton hosted on the stopped node cannot). // // NOT MeshGrpcBoundaryTests: that class proves a command only arrives if the target is // registered under its registry key, which is necessary but not discriminating — with one node // per side, per-node and singleton registration look identical. }