diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.ControlPlane/AdminOperations/AdminOperationsActor.cs b/src/Server/ZB.MOM.WW.OtOpcUa.ControlPlane/AdminOperations/AdminOperationsActor.cs
index cea2f062..eb31b42e 100644
--- a/src/Server/ZB.MOM.WW.OtOpcUa.ControlPlane/AdminOperations/AdminOperationsActor.cs
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.ControlPlane/AdminOperations/AdminOperationsActor.cs
@@ -85,6 +85,10 @@ public sealed class AdminOperationsActor : ReceiveActor
// derivation, cross-cluster/namespace binding, driver-namespace compat, signal collisions,
// …) gates the deploy. A green build in this repo does not prove the config is valid; this
// is the last guard before a bad address space (or a non-derived EquipmentId) ships.
+ // NOTE: a BadDuplicateExternalIdentifier rejection from the reservation pre-flight can
+ // (rarely) be a false positive if an external-id reservation release has not yet been
+ // committed/visible when the snapshot is read — operators seeing a spurious one should
+ // check ExternalIdReservation state before re-submitting.
var draft = await DraftSnapshotFactory.FromConfigDbAsync(db);
var errors = DraftValidator.Validate(draft);
if (errors.Count > 0)
diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests/AdminOperationsActorTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests/AdminOperationsActorTests.cs
index 8898c43c..39840f33 100644
--- a/tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests/AdminOperationsActorTests.cs
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests/AdminOperationsActorTests.cs
@@ -141,6 +141,54 @@ public sealed class AdminOperationsActorTests : ControlPlaneActorTestBase
verify.Deployments.Count().ShouldBe(0);
}
+ /// Verifies that a DriverInstance whose NamespaceId lives in a different cluster
+ /// triggers BadCrossClusterNamespaceBinding and routes to the Rejected branch through
+ /// the actor — no coordinator dispatch, no Deployment row.
+ /// Seeded: Namespace in cluster "MAIN" + DriverInstance in cluster "SITE-A" referencing that
+ /// namespace. NamespaceKind.Equipment + DriverType "ModbusTcp" satisfies the compat rule so
+ /// only the cross-cluster rule fires.
+ [Fact]
+ public void StartDeployment_rejects_on_cross_cluster_namespace_binding()
+ {
+ const string nsId = "ns-main-equipment";
+
+ var dbFactory = NewInMemoryDbFactory();
+ using (var db = dbFactory.CreateDbContext())
+ {
+ db.Namespaces.Add(new Configuration.Entities.Namespace
+ {
+ NamespaceId = nsId,
+ ClusterId = "MAIN",
+ Kind = Configuration.Enums.NamespaceKind.Equipment,
+ NamespaceUri = "urn:zb:main:equipment",
+ });
+ db.DriverInstances.Add(new Configuration.Entities.DriverInstance
+ {
+ DriverInstanceId = "drv-site-a-01",
+ ClusterId = "SITE-A",
+ NamespaceId = nsId, // cross-cluster: drv is SITE-A, ns is MAIN
+ Name = "site-a-modbus",
+ DriverType = "ModbusTcp", // compatible with Equipment ns — no compat error
+ DriverConfig = "{}",
+ });
+ db.SaveChanges();
+ }
+
+ var coordinator = CreateTestProbe("coord");
+ var actor = Sys.ActorOf(AdminOperationsActor.Props(dbFactory, coordinator.Ref, Enumerable.Empty()));
+
+ actor.Tell(new StartDeployment("joe", CorrelationId.NewId()));
+
+ coordinator.ExpectNoMsg(TimeSpan.FromMilliseconds(500));
+ var reply = ExpectMsg(TimeSpan.FromSeconds(3));
+ reply.Outcome.ShouldBe(StartDeploymentOutcome.Rejected);
+ reply.Message.ShouldNotBeNull();
+ reply.Message.ShouldContain("BadCrossClusterNamespaceBinding");
+
+ using var verify = dbFactory.CreateDbContext();
+ verify.Deployments.Count().ShouldBe(0);
+ }
+
/// Verifies that starting a deployment is refused when another is in flight.
[Fact]
public void StartDeployment_refuses_when_another_is_in_flight()