fix(runtime): flag cross-cluster orphan-equipment bindings on rebuild
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

ParseComposition(blob, nodeId, onInconsistency?) detects a kept equipment whose
UNS line belongs to another cluster (a same-cluster-invariant violation that
would orphan the equipment folder) and reports it via an optional callback,
wired to OpcUaPublishActor's logger. Detection-only; the upstream draft
validator remains the authority. Adds two unit tests.
This commit is contained in:
Joseph Doherty
2026-06-07 08:24:11 -04:00
parent b0a62a9f3b
commit 1c579410cd
3 changed files with 77 additions and 5 deletions
@@ -389,4 +389,51 @@ public sealed class DeploymentArtifactTests
DeploymentArtifact.ParseComposition(blob, "anything:4053").DriverInstancePlans.Count
.ShouldBe(DeploymentArtifact.ParseComposition(blob).DriverInstancePlans.Count);
}
/// <summary>An artifact where an equipment's driver is in the node's cluster but its UNS line's area
/// is in another cluster: <paramref name="areaCluster"/> controls the area's ClusterId.</summary>
private static byte[] OrphanEquipmentBlob(string areaCluster) => BlobOf(new
{
Clusters = new[] { new { ClusterId = "MAIN" }, new { ClusterId = "SITE-A" } },
Nodes = new[] { new { NodeId = "central-1:4053", ClusterId = "MAIN" } },
DriverInstances = new[]
{
new { DriverInstanceId = "main-driver", DriverType = "Modbus", DriverConfig = "{}", ClusterId = "MAIN", NamespaceId = "main-ns" },
},
UnsAreas = new[] { new { UnsAreaId = "area-1", Name = "Area1", ClusterId = areaCluster } },
UnsLines = new[] { new { UnsLineId = "line-1", UnsAreaId = "area-1", Name = "Line1" } },
Equipment = new[]
{
new { EquipmentId = "equip-1", Name = "Equip1", UnsLineId = "line-1", DriverInstanceId = "main-driver" },
},
});
/// <summary>Verifies the inconsistency callback fires when a kept equipment's UNS line belongs to
/// another cluster (a cross-cluster orphan binding).</summary>
[Fact]
public void ParseComposition_scoped_flags_cross_cluster_orphan_equipment()
{
var warnings = new List<string>();
var comp = DeploymentArtifact.ParseComposition(
OrphanEquipmentBlob(areaCluster: "SITE-A"), "central-1:4053", warnings.Add);
comp.EquipmentNodes.Select(e => e.EquipmentId).ShouldBe(new[] { "equip-1" }); // still returned
warnings.Count.ShouldBe(1);
warnings[0].ShouldContain("equip-1");
warnings[0].ShouldContain("line-1");
}
/// <summary>Verifies the inconsistency callback does NOT fire when the equipment, its driver, and its
/// UNS line/area are all in the same cluster (the normal, invariant-respecting case).</summary>
[Fact]
public void ParseComposition_scoped_consistent_equipment_does_not_warn()
{
var warnings = new List<string>();
var comp = DeploymentArtifact.ParseComposition(
OrphanEquipmentBlob(areaCluster: "MAIN"), "central-1:4053", warnings.Add);
comp.EquipmentNodes.Select(e => e.EquipmentId).ShouldBe(new[] { "equip-1" });
comp.UnsLines.Select(l => l.UnsLineId).ShouldBe(new[] { "line-1" });
warnings.ShouldBeEmpty();
}
}