test(deploy): cover cross-cluster rejection through the actor; note reservation false-positive at gate
v2-ci / build (push) Failing after 42s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped

This commit is contained in:
Joseph Doherty
2026-06-07 11:28:17 -04:00
parent 2676fc17b5
commit f078d41a8b
2 changed files with 52 additions and 0 deletions
@@ -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)
@@ -141,6 +141,54 @@ public sealed class AdminOperationsActorTests : ControlPlaneActorTestBase
verify.Deployments.Count().ShouldBe(0);
}
/// <summary>Verifies that a DriverInstance whose NamespaceId lives in a different cluster
/// triggers <c>BadCrossClusterNamespaceBinding</c> 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.</summary>
[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<IDriverProbe>()));
actor.Tell(new StartDeployment("joe", CorrelationId.NewId()));
coordinator.ExpectNoMsg(TimeSpan.FromMilliseconds(500));
var reply = ExpectMsg<StartDeploymentResult>(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);
}
/// <summary>Verifies that starting a deployment is refused when another is in flight.</summary>
[Fact]
public void StartDeployment_refuses_when_another_is_in_flight()