diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverHostActor.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverHostActor.cs
index 010aad9a..d2e1a971 100644
--- a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverHostActor.cs
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverHostActor.cs
@@ -364,7 +364,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers
return;
}
- var specs = DeploymentArtifact.ParseDriverInstances(blob);
+ var specs = DeploymentArtifact.ParseDriverInstances(blob, _localNode.Value);
var snapshots = _children.ToDictionary(
kv => kv.Key,
kv => new DriverChildSnapshot(kv.Value.DriverType, kv.Value.LastConfigJson),
@@ -429,7 +429,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers
Phase7CompositionResult composition;
try
{
- composition = DeploymentArtifact.ParseComposition(blob);
+ composition = DeploymentArtifact.ParseComposition(blob, _localNode.Value);
}
catch (Exception ex)
{
diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DriverHostActorReconcileTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DriverHostActorReconcileTests.cs
index 0e89a2fb..f6c8a709 100644
--- a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DriverHostActorReconcileTests.cs
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DriverHostActorReconcileTests.cs
@@ -132,6 +132,86 @@ public sealed class DriverHostActorReconcileTests : RuntimeActorTestBase
snap.Drivers.Count.ShouldBe(1);
}
+ ///
+ /// Verifies per-ClusterId scoping at the actor level: a 2-cluster artifact (MAIN + SITE-A,
+ /// one driver each) dispatched to a node whose ClusterNode row puts it in SITE-A spawns ONLY
+ /// the SITE-A driver — and the node still reaches Applied (the ack fires unconditionally even
+ /// when a node's cluster slice is empty).
+ ///
+ [Fact]
+ public void DriverHostActor_spawns_only_its_clusters_drivers()
+ {
+ var db = NewInMemoryDbFactory();
+ var factory = new CountingDriverFactory("Modbus");
+ // Both drivers are Modbus so the factory could create either — scoping, not type support,
+ // must be what excludes the MAIN driver.
+ var deploymentId = SeedMultiClusterDeployment(db, RevA,
+ ("main-modbus", "Modbus", "{}", true, "MAIN"),
+ ("sa-modbus", "Modbus", "{}", true, "SITE-A"));
+
+ // This node belongs to SITE-A per the Nodes (ClusterNode) rows.
+ var siteANode = NodeId.Parse("site-a-1:4053");
+ var coordinator = CreateTestProbe();
+ var actor = Sys.ActorOf(DriverHostActor.Props(
+ db, siteANode, coordinator.Ref,
+ driverFactory: factory,
+ localRoles: new HashSet { "driver" }));
+
+ actor.Tell(new DispatchDeployment(deploymentId, RevA, CorrelationId.NewId()));
+
+ // The node still reaches Applied even though it hosts only its own cluster's slice.
+ coordinator.ExpectMsg(TimeSpan.FromSeconds(5)).Outcome.ShouldBe(ApplyAckOutcome.Applied);
+
+ // Only the SITE-A driver was constructed — the MAIN driver was filtered out by scoping.
+ AwaitAssert(() => factory.CreateCount.ShouldBe(1), duration: TimeSpan.FromSeconds(3));
+
+ actor.Tell(new GetDiagnostics(CorrelationId.NewId()), coordinator.Ref);
+ var snap = coordinator.ExpectMsg(TimeSpan.FromSeconds(2));
+ snap.Drivers.Count.ShouldBe(1);
+ snap.Drivers[0].Name.ShouldBe("sa-modbus");
+ }
+
+ private static DeploymentId SeedMultiClusterDeployment(
+ IDbContextFactory db,
+ RevisionHash rev,
+ params (string Id, string Type, string Config, bool Enabled, string ClusterId)[] drivers)
+ {
+ var artifact = JsonSerializer.SerializeToUtf8Bytes(new
+ {
+ // >1 cluster + matching Nodes rows triggers ScopeTo (single-cluster would resolve to None).
+ Clusters = new[] { new { ClusterId = "MAIN" }, new { ClusterId = "SITE-A" } },
+ Nodes = new[]
+ {
+ new { NodeId = "central-1:4053", ClusterId = "MAIN" },
+ new { NodeId = "site-a-1:4053", ClusterId = "SITE-A" },
+ },
+ DriverInstances = drivers.Select(d => new
+ {
+ DriverInstanceRowId = Guid.NewGuid(),
+ DriverInstanceId = d.Id,
+ Name = d.Id,
+ DriverType = d.Type,
+ Enabled = d.Enabled,
+ DriverConfig = d.Config,
+ ClusterId = d.ClusterId,
+ }).ToArray(),
+ });
+
+ var id = DeploymentId.NewId();
+ using var ctx = db.CreateDbContext();
+ ctx.Deployments.Add(new Deployment
+ {
+ DeploymentId = id.Value,
+ RevisionHash = rev.Value,
+ Status = DeploymentStatus.Sealed,
+ CreatedBy = "test",
+ SealedAtUtc = DateTime.UtcNow,
+ ArtifactBlob = artifact,
+ });
+ ctx.SaveChanges();
+ return id;
+ }
+
private static DeploymentId SeedDeploymentWithDrivers(
IDbContextFactory db,
RevisionHash rev,