feat(cluster): refuse to boot a split-topology node on a DPS transport mode

Per-cluster mesh Phase 6 (Task 5). A node carrying a cluster-{ClusterId}
role (RoleParser.IsClusterRole) is in the split topology, where the
Phase 2/3/5 DPS dark-switch branches deliver nothing across the mesh
boundary. SplitTopologyTransportValidator fails host start unless such a
node uses the mesh-crossing transports: MeshTransport:Mode=ClusterClient
always; Telemetry:Mode=Grpc if it has the driver role; TelemetryDial:Mode
=Grpc if it has the admin role. It is a no-op for any node with no
cluster-role (legacy / single-mesh / test). Roles + the three modes are
cross-read from IConfiguration, mirroring ConfigSourceOptionsValidator.
Registered via AddValidatedOptions on MeshTransportOptions with
ValidateOnStart, alongside the sibling validators.

Claude-Session: https://claude.ai/code/session_01GASWkNEi68FSCtvr6rLoEW
This commit is contained in:
Joseph Doherty
2026-07-24 02:13:47 -04:00
parent d98b32640b
commit a886d5e6e0
3 changed files with 312 additions and 0 deletions
@@ -0,0 +1,187 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
using Shouldly;
using Xunit;
namespace ZB.MOM.WW.OtOpcUa.Cluster.Tests;
/// <summary>
/// A split-topology node (one carrying a <c>cluster-{ClusterId}</c> role) left on a DPS transport
/// mode does not error — the transport simply delivers nothing across the mesh boundary, and the
/// symptom is a deployment that never applies or telemetry that never arrives. These tests make
/// that misconfiguration a loud host-start failure instead. A node with no cluster-role is exempt
/// (legacy / single-mesh / test), and the validator is a no-op for it.
/// </summary>
public class SplitTopologyTransportValidatorTests
{
private static ValidateOptionsResult Validate(
string? meshMode,
string? telemetryMode,
string? telemetryDialMode,
params string[] roles)
{
var pairs = new Dictionary<string, string?>();
for (var i = 0; i < roles.Length; i++)
{
pairs[$"Cluster:Roles:{i}"] = roles[i];
}
if (meshMode is not null) pairs["MeshTransport:Mode"] = meshMode;
if (telemetryMode is not null) pairs["Telemetry:Mode"] = telemetryMode;
if (telemetryDialMode is not null) pairs["TelemetryDial:Mode"] = telemetryDialMode;
var configuration = new ConfigurationBuilder().AddInMemoryCollection(pairs).Build();
// The validated options instance is never read (all four values come from IConfiguration), so a
// default MeshTransportOptions is a fine stand-in.
return new SplitTopologyTransportValidator(configuration)
.Validate(MeshTransportOptions.SectionName, new MeshTransportOptions());
}
[Fact]
public void Cluster_role_driver_node_on_Dps_mesh_transport_fails()
{
// Red-before-green anchor: a split-topology node on DistributedPubSub cannot deliver commands.
var result = Validate(
meshMode: MeshTransportOptions.ModeDps,
telemetryMode: TelemetryOptions.ModeGrpc,
telemetryDialMode: null,
"driver", "cluster-SITE-A");
result.Failed.ShouldBeTrue();
result.FailureMessage.ShouldContain("MeshTransport:Mode");
result.FailureMessage.ShouldContain(MeshTransportOptions.ModeClusterClient);
result.FailureMessage.ShouldContain("SITE-A");
}
[Fact]
public void Cluster_role_driver_node_fully_configured_passes()
{
Validate(
meshMode: MeshTransportOptions.ModeClusterClient,
telemetryMode: TelemetryOptions.ModeGrpc,
telemetryDialMode: null,
"driver", "cluster-SITE-A").Succeeded.ShouldBeTrue();
}
[Fact]
public void Cluster_role_admin_node_fully_configured_passes()
{
Validate(
meshMode: MeshTransportOptions.ModeClusterClient,
telemetryMode: null,
telemetryDialMode: TelemetryDialOptions.ModeGrpc,
"admin", "cluster-SITE-A").Succeeded.ShouldBeTrue();
}
[Fact]
public void Cluster_role_driver_node_on_Dps_telemetry_fails()
{
var result = Validate(
meshMode: MeshTransportOptions.ModeClusterClient,
telemetryMode: TelemetryOptions.ModeDps,
telemetryDialMode: null,
"driver", "cluster-SITE-A");
result.Failed.ShouldBeTrue();
result.FailureMessage.ShouldContain("Telemetry:Mode");
result.FailureMessage.ShouldContain(TelemetryOptions.ModeGrpc);
}
[Fact]
public void Cluster_role_admin_node_on_Dps_telemetry_dial_fails()
{
var result = Validate(
meshMode: MeshTransportOptions.ModeClusterClient,
telemetryMode: null,
telemetryDialMode: TelemetryDialOptions.ModeDps,
"admin", "cluster-SITE-A");
result.Failed.ShouldBeTrue();
result.FailureMessage.ShouldContain("TelemetryDial:Mode");
result.FailureMessage.ShouldContain(TelemetryDialOptions.ModeGrpc);
}
[Fact]
public void Fused_cluster_role_node_needs_all_three_to_pass()
{
Validate(
meshMode: MeshTransportOptions.ModeClusterClient,
telemetryMode: TelemetryOptions.ModeGrpc,
telemetryDialMode: TelemetryDialOptions.ModeGrpc,
"admin", "driver", "cluster-MAIN").Succeeded.ShouldBeTrue();
}
[Fact]
public void Fused_cluster_role_node_missing_telemetry_grpc_fails()
{
var result = Validate(
meshMode: MeshTransportOptions.ModeClusterClient,
telemetryMode: TelemetryOptions.ModeDps,
telemetryDialMode: TelemetryDialOptions.ModeGrpc,
"admin", "driver", "cluster-MAIN");
result.Failed.ShouldBeTrue();
result.FailureMessage.ShouldContain("Telemetry:Mode");
}
[Fact]
public void Fused_cluster_role_node_missing_telemetry_dial_grpc_fails()
{
var result = Validate(
meshMode: MeshTransportOptions.ModeClusterClient,
telemetryMode: TelemetryOptions.ModeGrpc,
telemetryDialMode: TelemetryDialOptions.ModeDps,
"admin", "driver", "cluster-MAIN");
result.Failed.ShouldBeTrue();
result.FailureMessage.ShouldContain("TelemetryDial:Mode");
}
[Fact]
public void Non_cluster_role_node_on_all_Dps_passes()
{
// The exemption that keeps the validator a no-op for every legacy / single-mesh / test node:
// no cluster-{ClusterId} role means the split-topology rule does not apply at all.
Validate(
meshMode: MeshTransportOptions.ModeDps,
telemetryMode: TelemetryOptions.ModeDps,
telemetryDialMode: TelemetryDialOptions.ModeDps,
"admin", "driver").Succeeded.ShouldBeTrue();
}
[Fact]
public void Node_with_no_roles_on_all_Dps_passes()
{
// Absolute default: nothing declared, nothing to enforce.
Validate(
meshMode: MeshTransportOptions.ModeDps,
telemetryMode: TelemetryOptions.ModeDps,
telemetryDialMode: TelemetryDialOptions.ModeDps).Succeeded.ShouldBeTrue();
}
[Fact]
public void Unset_modes_on_a_cluster_role_node_fail_as_the_Dps_default()
{
// A split node that never sets the mode keys inherits the Dps defaults — which the split
// topology cannot use. Leaving them unset must fail exactly as setting them to Dps would.
var result = Validate(
meshMode: null,
telemetryMode: null,
telemetryDialMode: null,
"admin", "driver", "cluster-MAIN");
result.Failed.ShouldBeTrue();
result.FailureMessage.ShouldContain("MeshTransport:Mode");
}
[Fact]
public void Mode_matching_is_case_insensitive()
{
Validate(
meshMode: "clusterclient",
telemetryMode: "grpc",
telemetryDialMode: "grpc",
"admin", "driver", "cluster-MAIN").Succeeded.ShouldBeTrue();
}
}