using Akka.Actor; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Commons.Messages.Mesh; namespace ZB.MOM.WW.OtOpcUa.Host.IntegrationTests; /// /// Pins the two mesh comm-actor paths against a really booted host. /// /// /// /// /user/central-communication and /user/node-communication are the wire /// contract of the central↔node boundary: central sends to a path string, and the node /// registers under one. Nothing else in the codebase asserts they agree. Renaming either /// actor would compile, pass every unit test, deploy — and deliver nothing, with no error /// on either side, because a ClusterClient send to an unregistered path is simply dropped. /// /// /// Booting the real host is the point. A test that spawned the actors itself would pin the /// constant against itself and prove nothing about the wiring in /// WithOtOpcUaControlPlaneSingletons / WithOtOpcUaRuntimeActors. /// /// public class MeshCommActorPathTests { private static async Task AssertActorExistsAsync(ActorSystem system, string path) { var identity = await system.ActorSelection(path) .Ask(new Identify(path), TimeSpan.FromSeconds(10)); identity.Subject.ShouldNotBeNull( $"{path} is the ClusterClient wire contract for the central↔node boundary; nothing else " + "pins it, and a mismatch delivers nothing without erroring on either side"); } [Fact] public async Task Both_mesh_comm_actors_exist_at_their_contract_paths() { 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.CentralCommunication); await AssertActorExistsAsync(harness.NodeASystem, MeshPaths.NodeCommunication); await AssertActorExistsAsync(harness.NodeBSystem, MeshPaths.CentralCommunication); await AssertActorExistsAsync(harness.NodeBSystem, MeshPaths.NodeCommunication); } // 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 MeshClusterClientBoundaryTests: that class proves a send only arrives if the target is // registered with the receptionist, which is necessary but not discriminating — with one node // per side, per-node and singleton registration look identical. }