refactor(mesh): step 6 — delete the ClusterClient mode value; split topology now requires Grpc

MeshTransportOptions.ModeClusterClient is gone. MeshTransportOptionsValidator accepts Dps or
Grpc and nothing else, so a node left carrying the string "ClusterClient" is now REFUSED at
boot naming MeshTransport:Mode and both remaining values — deliberately fail-closed rather
than falling back to Dps, which on a split-topology node would deliver nothing at all. A new
fact pins exactly that.

SplitTopologyTransportValidator tightens from an allow-list of two to Grpc only, as its own
comment said it would once ClusterClient was deleted. Dps on a cluster-role node stays
refused; the rule is still an explicit allow-list, so a typo'd mode fails too.

Tests: the two ClusterClient-mode facts in MeshTransportOptionsValidatorTests are deleted
(their Dps siblings already assert the same rule for the one remaining non-Grpc mode), the
SplitTopology fixtures move to Grpc, and the three-mode Theories in
ControlPlaneStarterOrderTests / CentralCommunicationActorTests become two-mode.
This commit is contained in:
Joseph Doherty
2026-08-06 15:49:42 -04:00
parent 85ca86e955
commit af3b0c3146
7 changed files with 74 additions and 82 deletions
@@ -71,15 +71,26 @@ public class MeshTransportOptionsValidatorTests
}
[Fact]
public void Unknown_mode_message_names_all_three_accepted_values()
public void Unknown_mode_message_names_both_accepted_values()
{
// An operator who mistypes the mode gets the full menu, not a partial one — Grpc became a
// legal value in Phase 0 and an error that omitted it would send them back to ClusterClient.
var result = Validate(new MeshTransportOptions { Mode = "receptionist" });
// An operator who mistypes the mode gets the full menu, not a partial one.
var result = Validate(new MeshTransportOptions { Mode = "not-a-mode" });
result.Failed.ShouldBeTrue();
result.FailureMessage.ShouldContain(MeshTransportOptions.ModeDps);
result.FailureMessage.ShouldContain(MeshTransportOptions.ModeClusterClient);
result.FailureMessage.ShouldContain(MeshTransportOptions.ModeGrpc);
}
[Fact]
public void The_retired_ClusterClient_mode_fails_rather_than_falling_back_to_Dps()
{
// A node left carrying the deleted transport's mode value must be refused at boot. Silently
// treating it as Dps would leave a split-topology node running, listening, and delivering
// nothing across its mesh boundary — the exact absence this validator exists to prevent.
var result = Validate(new MeshTransportOptions { Mode = "ClusterClient" });
result.Failed.ShouldBeTrue();
result.FailureMessage.ShouldContain("MeshTransport:Mode");
result.FailureMessage.ShouldContain(MeshTransportOptions.ModeGrpc);
}
@@ -293,15 +304,6 @@ public class MeshTransportOptionsValidatorTests
"admin", "cluster-MAIN").Succeeded.ShouldBeTrue();
}
[Fact]
public void ClusterClient_admin_node_needs_no_config_serve_listener()
{
// Fails only if the ConfigServe rule wrongly fires outside Grpc mode.
Validate(
new MeshTransportOptions { Mode = MeshTransportOptions.ModeClusterClient },
"admin", "cluster-MAIN").Succeeded.ShouldBeTrue();
}
[Fact]
public void The_admin_role_is_also_read_from_OTOPCUA_ROLES()
{
@@ -348,14 +350,6 @@ public class MeshTransportOptionsValidatorTests
"driver", "cluster-SITE-A").Succeeded.ShouldBeTrue();
}
[Fact]
public void ClusterClient_mode_requires_none_of_the_new_Grpc_keys()
{
Validate(
new MeshTransportOptions { Mode = MeshTransportOptions.ModeClusterClient },
"driver", "cluster-SITE-A").Succeeded.ShouldBeTrue();
}
[Fact]
public void Grpc_fused_admin_and_driver_node_must_satisfy_BOTH_surfaces()
{
@@ -51,7 +51,6 @@ public class SplitTopologyTransportValidatorTests
result.Failed.ShouldBeTrue();
result.FailureMessage.ShouldContain("MeshTransport:Mode");
result.FailureMessage.ShouldContain(MeshTransportOptions.ModeClusterClient);
result.FailureMessage.ShouldContain(MeshTransportOptions.ModeGrpc);
result.FailureMessage.ShouldContain("SITE-A");
}
@@ -59,9 +58,8 @@ public class SplitTopologyTransportValidatorTests
[Fact]
public void Cluster_role_driver_node_on_Grpc_mesh_transport_passes()
{
// The migration window: Grpc is the transport this rule exists to arrive at, so a rig node
// flipped to it must boot. Without this relaxation the very first Grpc rollout step — setting
// the mode on a split-topology node — is a host-start failure.
// Grpc is now the ONLY mode that satisfies this rule: it is the only transport that crosses
// a mesh boundary.
Validate(
meshMode: MeshTransportOptions.ModeGrpc,
telemetryMode: TelemetryOptions.ModeGrpc,
@@ -92,8 +90,9 @@ public class SplitTopologyTransportValidatorTests
[Fact]
public void Cluster_role_node_on_an_unrecognised_mesh_transport_still_fails()
{
// Widening the rule from one accepted value to two must not widen it to "anything that is not
// Dps" — a typo'd mode is still a mode that delivers nothing across the boundary.
// The rule is an explicit allow-list, not "anything that is not Dps" — a typo'd mode is
// still a mode that delivers nothing across the boundary. The retired 'ClusterClient' value
// lands here too.
var result = Validate(
meshMode: "receptionist",
telemetryMode: TelemetryOptions.ModeGrpc,
@@ -108,7 +107,7 @@ public class SplitTopologyTransportValidatorTests
public void Cluster_role_driver_node_fully_configured_passes()
{
Validate(
meshMode: MeshTransportOptions.ModeClusterClient,
meshMode: MeshTransportOptions.ModeGrpc,
telemetryMode: TelemetryOptions.ModeGrpc,
telemetryDialMode: null,
"driver", "cluster-SITE-A").Succeeded.ShouldBeTrue();
@@ -118,7 +117,7 @@ public class SplitTopologyTransportValidatorTests
public void Cluster_role_admin_node_fully_configured_passes()
{
Validate(
meshMode: MeshTransportOptions.ModeClusterClient,
meshMode: MeshTransportOptions.ModeGrpc,
telemetryMode: null,
telemetryDialMode: TelemetryDialOptions.ModeGrpc,
"admin", "cluster-SITE-A").Succeeded.ShouldBeTrue();
@@ -128,7 +127,7 @@ public class SplitTopologyTransportValidatorTests
public void Cluster_role_driver_node_on_Dps_telemetry_fails()
{
var result = Validate(
meshMode: MeshTransportOptions.ModeClusterClient,
meshMode: MeshTransportOptions.ModeGrpc,
telemetryMode: TelemetryOptions.ModeDps,
telemetryDialMode: null,
"driver", "cluster-SITE-A");
@@ -142,7 +141,7 @@ public class SplitTopologyTransportValidatorTests
public void Cluster_role_admin_node_on_Dps_telemetry_dial_fails()
{
var result = Validate(
meshMode: MeshTransportOptions.ModeClusterClient,
meshMode: MeshTransportOptions.ModeGrpc,
telemetryMode: null,
telemetryDialMode: TelemetryDialOptions.ModeDps,
"admin", "cluster-SITE-A");
@@ -156,7 +155,7 @@ public class SplitTopologyTransportValidatorTests
public void Fused_cluster_role_node_needs_all_three_to_pass()
{
Validate(
meshMode: MeshTransportOptions.ModeClusterClient,
meshMode: MeshTransportOptions.ModeGrpc,
telemetryMode: TelemetryOptions.ModeGrpc,
telemetryDialMode: TelemetryDialOptions.ModeGrpc,
"admin", "driver", "cluster-MAIN").Succeeded.ShouldBeTrue();
@@ -166,7 +165,7 @@ public class SplitTopologyTransportValidatorTests
public void Fused_cluster_role_node_missing_telemetry_grpc_fails()
{
var result = Validate(
meshMode: MeshTransportOptions.ModeClusterClient,
meshMode: MeshTransportOptions.ModeGrpc,
telemetryMode: TelemetryOptions.ModeDps,
telemetryDialMode: TelemetryDialOptions.ModeGrpc,
"admin", "driver", "cluster-MAIN");
@@ -179,7 +178,7 @@ public class SplitTopologyTransportValidatorTests
public void Fused_cluster_role_node_missing_telemetry_dial_grpc_fails()
{
var result = Validate(
meshMode: MeshTransportOptions.ModeClusterClient,
meshMode: MeshTransportOptions.ModeGrpc,
telemetryMode: TelemetryOptions.ModeGrpc,
telemetryDialMode: TelemetryDialOptions.ModeDps,
"admin", "driver", "cluster-MAIN");
@@ -229,7 +228,7 @@ public class SplitTopologyTransportValidatorTests
public void Mode_matching_is_case_insensitive()
{
Validate(
meshMode: "clusterclient",
meshMode: "grpc",
telemetryMode: "grpc",
telemetryDialMode: "grpc",
"admin", "driver", "cluster-MAIN").Succeeded.ShouldBeTrue();
@@ -287,13 +286,13 @@ public class SplitTopologyTransportValidatorTests
}
[Fact]
public void Cluster_role_node_with_neither_admin_nor_driver_only_needs_ClusterClient()
public void Cluster_role_node_with_neither_admin_nor_driver_only_needs_the_mesh_mode()
{
// A cluster-role node that is neither admin nor driver has no telemetry surface to constrain —
// only the always-on MeshTransport=ClusterClient rule applies. Telemetry/TelemetryDial left on
// only the always-on MeshTransport=Grpc rule applies. Telemetry/TelemetryDial left on
// their Dps defaults must not fail it.
Validate(
meshMode: MeshTransportOptions.ModeClusterClient,
meshMode: MeshTransportOptions.ModeGrpc,
telemetryMode: null,
telemetryDialMode: null,
"cluster-SITE-A").Succeeded.ShouldBeTrue();
@@ -515,17 +515,16 @@ public class CentralCommunicationActorTests : ControlPlaneActorTestBase
[Theory]
[InlineData(MeshTransportOptions.ModeDps)]
[InlineData(MeshTransportOptions.ModeClusterClient)]
[InlineData(MeshTransportOptions.ModeGrpc)]
public void The_known_cluster_gate_is_published_in_every_transport_mode(string mode)
{
// Replaces an earlier fact asserting the OPPOSITE, whose premise did not survive Phase 1B: the
// gate feeds CentralCommandAuthInterceptor, which is registered on every admin node, and the
// ack service it protects is mapped by LISTENER PORT — neither consults MeshTransport:Mode.
// Gating the publication on Grpc meant a dark-switch rollback to Dps left that endpoint
// serving with a permanently inactive gate, silently restoring both the timing
// oracle and the pre-authentication store/audit flood. The set is central's cluster
// inventory, not transport state.
// Gating the publication on Grpc meant a rollback to Dps left that endpoint serving with a
// permanently inactive gate, silently restoring both the timing oracle and the
// pre-authentication store/audit flood. The set is central's cluster inventory, not
// transport state.
var factory = NewInMemoryDbFactory();
SeedGrpcCluster(factory, "C1", ("host-a:4053", "host-a", 4056, true, false));
var psk = new RecordingPskProvider();
@@ -55,11 +55,10 @@ public sealed class ControlPlaneStarterOrderTests
{
[Theory]
[InlineData(MeshTransportOptions.ModeDps)]
[InlineData(MeshTransportOptions.ModeClusterClient)]
[InlineData(MeshTransportOptions.ModeGrpc)]
public async Task An_admin_node_boots_with_the_real_mesh_dependency_graph(string mode)
{
// Every mode, because the eager resolve was mode-independent: the starter resolved both
// Both modes, because the eager resolve was mode-independent: the starter resolved both
// dependencies before anything looked at MeshTransport:Mode, so Dps — the DEFAULT, and the
// rollback target — died exactly like Grpc did.
using var host = BuildAdminHost(mode);
@@ -82,7 +81,6 @@ public sealed class ControlPlaneStarterOrderTests
[Theory]
[InlineData(MeshTransportOptions.ModeDps)]
[InlineData(MeshTransportOptions.ModeClusterClient)]
[InlineData(MeshTransportOptions.ModeGrpc)]
public async Task The_deferred_dependencies_are_resolved_and_used_once_the_host_is_up(string mode)
{
@@ -90,7 +88,7 @@ public sealed class ControlPlaneStarterOrderTests
// silent one, with the known-cluster gate permanently unpublished and the dialer permanently
// absent. So this asserts the far end of the deferral: the actor resolved the PSK provider
// FROM THE CONTAINER and drove it, through the ordinary contact-refresh path. A Theory over
// all three modes because publication is mode-independent (the gate follows the endpoint).
// both modes because publication is mode-independent (the gate follows the endpoint).
//
// The provider is registered as a TYPE with an ISecretResolver constructor, so the resolve
// walks the same registry-bound chain the production MeshPskProvider does — which makes