4d2dfb5b50
MeshGrpcBoundaryTests replaces the MeshClusterClientBoundaryTests deleted in step 1. Three
self-seeded single-member clusters sharing the ActorSystem name, joined ONLY by gRPC, driving
the REAL CentralCommunicationActor -> MeshGrpcCommandDialer -> Kestrel h2c ->
MeshCommandAuthInterceptor -> NodeCommandGrpcService -> NodeCommunicationActor -> EventStream:
1. No_cross_mesh_Akka_association_forms_between_central_and_either_site — the program's own
point, kept as a test rather than a live-gate note: each cluster stays single-member and
none ever holds another's address as a member.
2. One_dispatch_through_central_reaches_a_node_in_every_cluster_mesh — the fan, with each
site holding its OWN per-cluster PSK, so a dial that selected the wrong cluster's key
would be refused at the far side.
3. Falsifiability_control_a_dial_carrying_the_wrong_cluster_key_delivers_nothing — without
it, (2) cannot distinguish "the gRPC boundary carried it" from "something else in the
process did". The same node then accepts the right key, so the silence is the auth gate
and not a dead endpoint.
All three run fully offline — no Docker fixture — and pass.
Comment sweep across every remaining clusterclient|receptionist hit in src/ and tests/:
the program label becomes "mesh gRPC command plane", and every comment that described the
deleted transport as live now describes what actually runs (Dps = pair-local default,
Grpc = cross-mesh). mesh_transport.proto's header/service comments are reworded; no message,
field or enum is touched — the contract is locked. MeshCommActorPathTests is reshaped rather
than retired: the two paths still exist and still matter, but what a mismatch costs is now a
misleading Unavailable from the gRPC ingress rather than a silently dropped receptionist send.
The only surviving mentions of the old names are the ones that must survive: the fail-closed
rules that name "ClusterClient" as a retired value refused at boot, their tests, and this
test class's own account of what it replaces.
68 lines
3.5 KiB
C#
68 lines
3.5 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 where each
|
|
/// side's gRPC ingress expects to find its comm actor: the services resolve them by registry
|
|
/// key, and these constants are the names the actors are spawned under. Nothing else in the
|
|
/// codebase asserts they agree. Renaming either actor would compile, pass every unit test,
|
|
/// deploy — and refuse every inbound call as <c>Unavailable</c> ("the node is still
|
|
/// starting"), which reads as a boot-window race rather than a permanent misnaming.
|
|
/// </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 where the central↔node boundary expects its comm actor; nothing else pins "
|
|
+ "it, and a mismatch turns every inbound call into a misleading readiness rejection");
|
|
}
|
|
|
|
[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 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.
|
|
}
|