diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/OpcUa/OpcUaPublishActor.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/OpcUa/OpcUaPublishActor.cs
index 9f005cc1..3d8a735c 100644
--- a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/OpcUa/OpcUaPublishActor.cs
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/OpcUa/OpcUaPublishActor.cs
@@ -209,7 +209,9 @@ public sealed class OpcUaPublishActor : ReceiveActor
var artifact = msg.DeploymentId is { } depId
? LoadArtifact(depId)
: LoadLatestArtifact();
- var composition = DeploymentArtifact.ParseComposition(artifact);
+ var composition = _localNode is { } ln
+ ? DeploymentArtifact.ParseComposition(artifact, ln.Value)
+ : DeploymentArtifact.ParseComposition(artifact);
var plan = Phase7Planner.Compute(_lastApplied, composition);
if (plan.IsEmpty)
diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/OpcUa/OpcUaPublishActorRebuildTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/OpcUa/OpcUaPublishActorRebuildTests.cs
index 618c1672..1a9760d7 100644
--- a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/OpcUa/OpcUaPublishActorRebuildTests.cs
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/OpcUa/OpcUaPublishActorRebuildTests.cs
@@ -98,6 +98,104 @@ public sealed class OpcUaPublishActorRebuildTests : RuntimeActorTestBase
AwaitAssert(() => sink.RebuildCalls.ShouldBe(1), duration: TimeSpan.FromMilliseconds(500));
}
+ ///
+ /// Wiring proof for per-ClusterId scoping (Task 4): a multi-cluster artifact must
+ /// materialise ONLY the local node's cluster slice. Mirrors the multi-cluster artifact
+ /// shape exercised in DeploymentArtifactTests (MAIN + SITE-A, one Galaxy driver +
+ /// one SystemPlatform tag each). The scoped rebuild for the SITE-A node must surface the
+ /// SITE-A tag (t-sa → variable F.S1) and NOT MAIN's (t-main →
+ /// F.M1); the mirror holds for the MAIN node. Without the production scoping edit,
+ /// the unscoped parse would materialise BOTH variables on every node.
+ ///
+ [Fact]
+ public void Rebuild_materialises_only_the_nodes_cluster()
+ {
+ // --- SITE-A node: only the SITE-A tag's variable, never MAIN's. ---
+ var dbA = NewInMemoryDbFactory();
+ var sinkA = new RecordingSink();
+ var applierA = new Phase7Applier(sinkA, NullLogger.Instance);
+ SeedMultiClusterDeployment(dbA);
+
+ var siteActor = Sys.ActorOf(OpcUaPublishActor.PropsForTests(
+ sink: sinkA,
+ dbFactory: dbA,
+ applier: applierA,
+ localNode: NodeId.Parse("site-a-1:4053")));
+
+ siteActor.Tell(new OpcUaPublishActor.RebuildAddressSpace(CorrelationId.NewId()));
+
+ AwaitAssert(() => sinkA.RebuildCalls.ShouldBe(1), duration: TimeSpan.FromSeconds(2));
+ // t-sa (Name "S1", FolderPath "F") → MxAccessRef "F.S1" → variable node "F.S1".
+ sinkA.Calls.ShouldContain("EV:F.S1");
+ // t-main (MAIN cluster) must NOT leak onto the SITE-A node.
+ sinkA.Calls.ShouldNotContain("EV:F.M1");
+
+ // --- MAIN node: the mirror — only MAIN's tag's variable, never SITE-A's. ---
+ var dbM = NewInMemoryDbFactory();
+ var sinkM = new RecordingSink();
+ var applierM = new Phase7Applier(sinkM, NullLogger.Instance);
+ SeedMultiClusterDeployment(dbM);
+
+ var mainActor = Sys.ActorOf(OpcUaPublishActor.PropsForTests(
+ sink: sinkM,
+ dbFactory: dbM,
+ applier: applierM,
+ localNode: NodeId.Parse("central-1:4053")));
+
+ mainActor.Tell(new OpcUaPublishActor.RebuildAddressSpace(CorrelationId.NewId()));
+
+ AwaitAssert(() => sinkM.RebuildCalls.ShouldBe(1), duration: TimeSpan.FromSeconds(2));
+ sinkM.Calls.ShouldContain("EV:F.M1");
+ sinkM.Calls.ShouldNotContain("EV:F.S1");
+ }
+
+ ///
+ /// Seal a 2-cluster deployment (MAIN + SITE-A) whose artifact mirrors the multi-cluster
+ /// shape the composer emits: a Clusters + Nodes map, one SystemPlatform
+ /// namespace + Galaxy driver + Galaxy tag per cluster. Used by
+ /// .
+ ///
+ private static void SeedMultiClusterDeployment(IDbContextFactory dbFactory)
+ {
+ var artifact = JsonSerializer.SerializeToUtf8Bytes(new
+ {
+ 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 = new[]
+ {
+ new { DriverInstanceId = "main-galaxy", DriverType = "GalaxyMxGateway", DriverConfig = "{}", ClusterId = "MAIN", NamespaceId = "main-ns" },
+ new { DriverInstanceId = "sa-galaxy", DriverType = "GalaxyMxGateway", DriverConfig = "{}", ClusterId = "SITE-A", NamespaceId = "sa-ns" },
+ },
+ Namespaces = new[]
+ {
+ new { NamespaceId = "main-ns", ClusterId = "MAIN", Kind = 1 }, // NamespaceKind.SystemPlatform
+ new { NamespaceId = "sa-ns", ClusterId = "SITE-A", Kind = 1 },
+ },
+ Tags = new[]
+ {
+ new { TagId = "t-main", DriverInstanceId = "main-galaxy", EquipmentId = (string?)null, Name = "M1", FolderPath = "F", DataType = "Boolean", TagConfig = "{}" },
+ new { TagId = "t-sa", DriverInstanceId = "sa-galaxy", EquipmentId = (string?)null, Name = "S1", FolderPath = "F", DataType = "Boolean", TagConfig = "{}" },
+ },
+ ScriptedAlarms = Array.Empty