5468b79d19
Two genuinely separate single-member clusters sharing the ActorSystem name and joined only by ClusterClient, so the mesh boundary is exercised over the wire rather than through an in-process relay: a relay cannot fail for a missing receptionist extension, an unregistered service, a malformed contact path or a serialization break, which are the Phase 2 failure modes. Covers both legs plus the control: * central SendToAll -> node comm actor -> node EventStream * node ApplyAck -> central comm actor, through the node's own client * a node without the receptionist extension receives nothing Sabotage-verified. Suppressing the node-side EventStream publish and the outbound ack Tell reddens one leg each. Restoring the receptionist and the service registration on the control node reddens the control, which is what proves the control's send is live rather than silently misaddressed. The control removes the extension AND the RegisterService call, since resolving the receptionist to register would materialise the extension whose absence is the point; it falsifies "delivery happens without a receptionist boundary", not the extension line alone. Recorded in the test. Also corrects MeshCommActorPathTests: with one node per side, per-node and singleton registration are indistinguishable here, so the property the dropped Task 6 assertion covered belongs to live-gate step 8, not to this class. Claude-Session: https://claude.ai/code/session_01GASWkNEi68FSCtvr6rLoEW
67 lines
3.4 KiB
C#
67 lines
3.4 KiB
C#
using Akka.Actor;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Commons.Messages.Mesh;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Host.IntegrationTests;
|
|
|
|
/// <summary>
|
|
/// Pins the two mesh comm-actor paths against a <b>really booted</b> host.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// <c>/user/central-communication</c> and <c>/user/node-communication</c> 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.
|
|
/// </para>
|
|
/// <para>
|
|
/// 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
|
|
/// <c>WithOtOpcUaControlPlaneSingletons</c> / <c>WithOtOpcUaRuntimeActors</c>.
|
|
/// </para>
|
|
/// </remarks>
|
|
public class MeshCommActorPathTests
|
|
{
|
|
private static async Task AssertActorExistsAsync(ActorSystem system, string path)
|
|
{
|
|
var identity = await system.ActorSelection(path)
|
|
.Ask<ActorIdentity>(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.
|
|
}
|