diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DiscoveryInjectionEndToEndTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DiscoveryInjectionEndToEndTests.cs index 1a60b9a4..82a378ce 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DiscoveryInjectionEndToEndTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DiscoveryInjectionEndToEndTests.cs @@ -74,6 +74,14 @@ public sealed class DiscoveryInjectionEndToEndTests : RuntimeActorTestBase private const string FixedTreeRef = "10.0.0.5:8193/Identity/SeriesNumber"; private const string FixedTreeDisplayName = "SeriesNumber"; + // The DETERMINISTIC NodeId the chain must place the FixedTree node at: EQ-1 (the bound equipment root) + + // the COLLAPSED folder path. The mapper's device-folder collapse drops the single shared device-host + // segment ("10.0.0.5:8193"), so FolderPathSegments ["FOCAS","10.0.0.5:8193","Identity"] + browse + // "SeriesNumber" → "EQ-1/FOCAS/Identity/SeriesNumber" (per EquipmentNodeIds.Variable). Asserting this + // EXACT NodeId closes the loop on the collapse rule — a prefix/StartsWith check would still pass if the + // collapse broke (e.g. "EQ-1/FOCAS/10.0.0.5:8193/Identity/SeriesNumber"). + private const string ExpectedFixedTreeNodeId = "EQ-1/FOCAS/Identity/SeriesNumber"; + private static DiscoveredNode[] FixedTreeNodes() => new[] { new DiscoveredNode( @@ -112,27 +120,25 @@ public sealed class DiscoveryInjectionEndToEndTests : RuntimeActorTestBase // Driver reports its captured FixedTree (the faithful Task-7/8 seam). host.Tell(new DriverInstanceActor.DiscoveredNodesReady("d1", FixedTreeNodes())); - // (a) The discovered variable was materialised through the REAL applier onto the sink, under EQ-1. - string fixedTreeNodeId = null!; + // (a) The discovered variable was materialised through the REAL applier onto the sink, at the EXACT + // collapsed NodeId under the bound equipment root (proves the mapper's device-folder collapse). AwaitAssert(() => { var v = sink.Variables.SingleOrDefault(x => x.DisplayName == FixedTreeDisplayName); - v.NodeId.ShouldNotBeNull(); // EnsureVariable recorded for the FixedTree node - v.NodeId.ShouldStartWith("EQ-1"); // grafted UNDER the bound equipment root - v.DataType.ShouldBe("String"); // mapper carried the driver type through to the sink - v.Writable.ShouldBeFalse(); // discovered nodes are read-only - fixedTreeNodeId = v.NodeId; - sink.ModelChanges.ShouldContain("EQ-1"); // NodeAdded announced under the equipment + v.NodeId.ShouldBe(ExpectedFixedTreeNodeId); // EnsureVariable at the exact collapsed NodeId under EQ-1 + v.DataType.ShouldBe("String"); // mapper carried the driver type through to the sink + v.Writable.ShouldBeFalse(); // discovered nodes are read-only + sink.ModelChanges.ShouldContain("EQ-1"); // NodeAdded announced under the equipment }, duration: Timeout); - // (b) A value published for the FixedTree ref routes to the mapped NodeId and lands Good — the live + // (b) A value published for the FixedTree ref routes to THAT exact NodeId and lands Good — the live // value flowed end-to-end (host routing map → publish actor → applier-backing sink WriteValue). host.Tell(new DriverInstanceActor.AttributeValuePublished("d1", FixedTreeRef, "SN-12345", OpcUaQuality.Good, Ts)); AwaitAssert(() => { - var write = sink.Values.SingleOrDefault(x => x.NodeId == fixedTreeNodeId); - write.NodeId.ShouldBe(fixedTreeNodeId); + var write = sink.Values.SingleOrDefault(x => x.NodeId == ExpectedFixedTreeNodeId); + write.NodeId.ShouldBe(ExpectedFixedTreeNodeId); write.Value.ShouldBe("SN-12345"); write.Quality.ShouldBe(OpcUaQuality.Good); write.Ts.ShouldBe(Ts); @@ -159,22 +165,18 @@ public sealed class DiscoveryInjectionEndToEndTests : RuntimeActorTestBase host.Tell(new DriverInstanceActor.DiscoveredNodesReady("d1", FixedTreeNodes())); - // First injection: capture the mapped NodeId once it has materialised on the sink. - string fixedTreeNodeId = null!; - AwaitAssert(() => - { - var v = sink.Variables.SingleOrDefault(x => x.DisplayName == FixedTreeDisplayName); - v.NodeId.ShouldNotBeNull(); - fixedTreeNodeId = v.NodeId; - }, duration: Timeout); + // First injection: the FixedTree node materialises at the EXACT collapsed NodeId under EQ-1. + AwaitAssert( + () => sink.Variables.ShouldContain(x => x.NodeId == ExpectedFixedTreeNodeId && x.DisplayName == FixedTreeDisplayName), + duration: Timeout); // First value flows Good (pre-redeploy baseline). host.Tell(new DriverInstanceActor.AttributeValuePublished("d1", FixedTreeRef, "SN-AAA", OpcUaQuality.Good, Ts)); AwaitAssert( - () => sink.Values.ShouldContain(x => x.NodeId == fixedTreeNodeId && Equals(x.Value, "SN-AAA") && x.Quality == OpcUaQuality.Good), + () => sink.Values.ShouldContain(x => x.NodeId == ExpectedFixedTreeNodeId && Equals(x.Value, "SN-AAA") && x.Quality == OpcUaQuality.Good), duration: Timeout); - var ensureVarCountBefore = sink.Variables.Count(x => x.NodeId == fixedTreeNodeId); + var ensureVarCountBefore = sink.Variables.Count(x => x.NodeId == ExpectedFixedTreeNodeId); // Apply a SECOND deployment (new revision, SAME d1 → EQ-1 binding) — re-runs PushDesiredSubscriptions // (clears + rebuilds the routing maps) then the Task-8 tail re-applies the cached discovered plan. @@ -183,17 +185,17 @@ public sealed class DiscoveryInjectionEndToEndTests : RuntimeActorTestBase host.Tell(new DispatchDeployment(deploymentId2, RevB, CorrelationId.NewId())); coordinator.ExpectMsg(Timeout).Outcome.ShouldBe(ApplyAckOutcome.Applied); - // (a) The cached discovered plan was RE-MATERIALISED at the SAME NodeId after the redeploy rebuild. + // (a) The cached discovered plan was RE-MATERIALISED at the SAME exact NodeId after the redeploy rebuild. AwaitAssert( - () => sink.Variables.Count(x => x.NodeId == fixedTreeNodeId).ShouldBeGreaterThan(ensureVarCountBefore), + () => sink.Variables.Count(x => x.NodeId == ExpectedFixedTreeNodeId).ShouldBeGreaterThan(ensureVarCountBefore), duration: Timeout); - // (b) A value published AFTER the redeploy STILL routes to the mapped NodeId and lands Good — the + // (b) A value published AFTER the redeploy STILL routes to the exact NodeId and lands Good — the // live-value routing map was rebuilt by the re-apply (not lost when PushDesiredSubscriptions cleared it). var tsAfter = Ts.AddSeconds(5); host.Tell(new DriverInstanceActor.AttributeValuePublished("d1", FixedTreeRef, "SN-BBB", OpcUaQuality.Good, tsAfter)); AwaitAssert( - () => sink.Values.ShouldContain(x => x.NodeId == fixedTreeNodeId && Equals(x.Value, "SN-BBB") && x.Quality == OpcUaQuality.Good), + () => sink.Values.ShouldContain(x => x.NodeId == ExpectedFixedTreeNodeId && Equals(x.Value, "SN-BBB") && x.Quality == OpcUaQuality.Good), duration: Timeout); }