diff --git a/src/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure/ClusterOptions.cs b/src/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure/ClusterOptions.cs
index 8e4c297b..c52f7edc 100644
--- a/src/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure/ClusterOptions.cs
+++ b/src/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure/ClusterOptions.cs
@@ -76,4 +76,12 @@ public class ClusterOptions
/// other reachable members, rather than running as an isolated single-node cluster.
///
public bool DownIfAlone { get; set; } = true;
+
+ ///
+ /// Acknowledges a deliberate single-node deployment: permits exactly one seed
+ /// node (normally ≥2 are required so either node can start first). Without this
+ /// flag a single-node install must list a phantom second seed, which the node
+ /// dials forever — log noise and a defeated validation intent (review 01).
+ ///
+ public bool AllowSingleNodeCluster { get; set; }
}
diff --git a/src/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure/ClusterOptionsValidator.cs b/src/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure/ClusterOptionsValidator.cs
index 2b30c65f..21abae02 100644
--- a/src/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure/ClusterOptionsValidator.cs
+++ b/src/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure/ClusterOptionsValidator.cs
@@ -26,11 +26,13 @@ public sealed class ClusterOptionsValidator : OptionsValidatorBase= 2,
- "ClusterOptions.SeedNodes must contain at least 2 seed nodes "
- + "(Component-ClusterInfrastructure.md → Node Configuration: "
- + "both nodes are seed nodes); a single-seed configuration defeats "
- + "the no-startup-ordering-dependency guarantee.");
+ var minSeeds = options.AllowSingleNodeCluster ? 1 : 2;
+ builder.RequireThat(options.SeedNodes is not null && options.SeedNodes.Count >= minSeeds,
+ options.AllowSingleNodeCluster
+ ? "ClusterOptions.SeedNodes must contain at least 1 seed node."
+ : "ClusterOptions.SeedNodes must contain at least 2 seed nodes "
+ + "(Component-ClusterInfrastructure.md → Node Configuration: both nodes are seed nodes); "
+ + "for a deliberate single-node install set ClusterOptions.AllowSingleNodeCluster = true instead of listing a phantom seed.");
builder.RequireThat(
!string.IsNullOrWhiteSpace(options.SplitBrainResolverStrategy)
diff --git a/tests/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure.Tests/ClusterOptionsValidatorTests.cs b/tests/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure.Tests/ClusterOptionsValidatorTests.cs
index 58cdbabd..deb05d3e 100644
--- a/tests/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure.Tests/ClusterOptionsValidatorTests.cs
+++ b/tests/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure.Tests/ClusterOptionsValidatorTests.cs
@@ -69,6 +69,47 @@ public class ClusterOptionsValidatorTests
Assert.Contains("SeedNodes", result.FailureMessage);
}
+ [Fact]
+ public void SingleSeed_WithoutAcknowledgement_Fails()
+ {
+ var options = ValidOptions();
+ options.SeedNodes = new List { "akka.tcp://scadabridge@h:8081" };
+
+ var result = new ClusterOptionsValidator().Validate(null, options);
+
+ Assert.True(result.Failed);
+ Assert.Contains("AllowSingleNodeCluster", result.FailureMessage);
+ }
+
+ [Fact]
+ public void SingleSeed_WithAllowSingleNodeCluster_Passes()
+ {
+ // Review 01 [Low]: the shipped single-node artifact satisfied the 2-seed
+ // rule with a phantom seed the node dials forever. An explicit
+ // acknowledgement flag replaces the fiction.
+ var options = ValidOptions();
+ options.SeedNodes = new List { "akka.tcp://scadabridge@h:8081" };
+ options.AllowSingleNodeCluster = true;
+
+ var result = new ClusterOptionsValidator().Validate(null, options);
+
+ Assert.True(result.Succeeded, result.FailureMessage);
+ }
+
+ [Fact]
+ public void EmptySeeds_FailsEvenWithFlag()
+ {
+ // Zero seeds is always invalid — the flag lowers the floor to 1, not 0.
+ var options = ValidOptions();
+ options.SeedNodes = new List();
+ options.AllowSingleNodeCluster = true;
+
+ var result = new ClusterOptionsValidator().Validate(null, options);
+
+ Assert.True(result.Failed);
+ Assert.Contains("SeedNodes", result.FailureMessage);
+ }
+
[Fact]
public void SingleSeedNode_FailsValidation()
{