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,