diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DeploymentArtifactArrayParityTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DeploymentArtifactArrayParityTests.cs index 61c5f511..52335e13 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DeploymentArtifactArrayParityTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DeploymentArtifactArrayParityTests.cs @@ -102,6 +102,9 @@ public sealed class DeploymentArtifactArrayParityTests // round-trip (it can't be unit-tested directly because it's private). A truly malformed TagConfig // string would cause ExtractTagFullName to return "" and break the SequenceEqual on other fields, // so the throws/malformed path is covered by the composer unit test, not here. + // NOTE (review M-1): only the string-type bad-length case is round-tripped here. The + // negative/float/overflow arrayLength reject paths are covered on the composer side + // (ExtractTagArrayTests) and do not need duplication in this artifact parity test. var arrayBadLengthTag = new Tag { TagId = "tag-array-badlen", @@ -113,12 +116,40 @@ public sealed class DeploymentArtifactArrayParityTests AccessLevel = TagAccessLevel.Read, TagConfig = "{\"FullName\":\"40004\",\"isArray\":true,\"arrayLength\":\"sixteen\"}", }; + // I-1: isArray:false with a non-zero arrayLength — the artifact-side guard must gate arrayLength + // under isArray, so the length is discarded and IsArray==false, ArrayLength==null on both sides. + // Pins that a future removal of the isArray gate would be caught here. + var arrayDisabledTag = new Tag + { + TagId = "tag-array-disabled", + DriverInstanceId = "drv-modbus", + EquipmentId = "eq-1", + FolderPath = null, + Name = "Setpoint", + DataType = "Float", + AccessLevel = TagAccessLevel.ReadWrite, + TagConfig = "{\"FullName\":\"40005\",\"isArray\":false,\"arrayLength\":8}", + }; + // I-2: isArray:true with arrayLength:0 — zero is a legal explicit dimension bound (e.g. dynamic + // array with declared-but-unset size). Must decode to IsArray==true, ArrayLength==0u on both sides. + // Pins that a future change treating 0 as absent would be caught here. + var arrayZeroLengthTag = new Tag + { + TagId = "tag-array-zerolen", + DriverInstanceId = "drv-modbus", + EquipmentId = "eq-1", + FolderPath = null, + Name = "Empty", + DataType = "Int32", + AccessLevel = TagAccessLevel.Read, + TagConfig = "{\"FullName\":\"40006\",\"isArray\":true,\"arrayLength\":0}", + }; var areas = new[] { area }; var lines = new[] { line }; var equipment = new[] { equip }; var drivers = new[] { driver }; - var tags = new[] { arrayBoundedTag, scalarTag, arrayUnboundedTag, arrayBadLengthTag }; + var tags = new[] { arrayBoundedTag, scalarTag, arrayUnboundedTag, arrayBadLengthTag, arrayDisabledTag, arrayZeroLengthTag }; var namespaces = new[] { ns }; // ---- Side 1: the live-edit composer ---- @@ -142,13 +173,15 @@ public sealed class DeploymentArtifactArrayParityTests ToSnapshot(scalarTag), ToSnapshot(arrayUnboundedTag), ToSnapshot(arrayBadLengthTag), + ToSnapshot(arrayDisabledTag), + ToSnapshot(arrayZeroLengthTag), }, }); var decoded = DeploymentArtifact.ParseComposition(blob); // ---- Full byte-parity: every field, same order (positional-record value equality) ---- - decoded.EquipmentTags.Count.ShouldBe(4); + decoded.EquipmentTags.Count.ShouldBe(6); decoded.EquipmentTags.SequenceEqual(composed.EquipmentTags).ShouldBeTrue(); // Spell out the array fields per-tag so a divergence names the offending tag. @@ -177,6 +210,22 @@ public sealed class DeploymentArtifactArrayParityTests arrayBadLength.ArrayLength.ShouldBeNull(); composed.EquipmentTags.Single(t => t.TagId == "tag-array-badlen").IsArray.ShouldBeTrue(); composed.EquipmentTags.Single(t => t.TagId == "tag-array-badlen").ArrayLength.ShouldBeNull(); + + // I-1: isArray:false with a non-zero arrayLength ⇒ (false, null) on both sides. + // The artifact-side isArray gate must suppress arrayLength even when it is present in the blob. + var arrayDisabled = decoded.EquipmentTags.Single(t => t.TagId == "tag-array-disabled"); + arrayDisabled.IsArray.ShouldBeFalse(); + arrayDisabled.ArrayLength.ShouldBeNull(); + composed.EquipmentTags.Single(t => t.TagId == "tag-array-disabled").IsArray.ShouldBeFalse(); + composed.EquipmentTags.Single(t => t.TagId == "tag-array-disabled").ArrayLength.ShouldBeNull(); + + // I-2: isArray:true with arrayLength:0 ⇒ (true, 0u) on both sides. + // Zero is a legal explicit dimension bound; it must not be treated as absent. + var arrayZeroLength = decoded.EquipmentTags.Single(t => t.TagId == "tag-array-zerolen"); + arrayZeroLength.IsArray.ShouldBeTrue(); + arrayZeroLength.ArrayLength.ShouldBe(0u); + composed.EquipmentTags.Single(t => t.TagId == "tag-array-zerolen").IsArray.ShouldBeTrue(); + composed.EquipmentTags.Single(t => t.TagId == "tag-array-zerolen").ArrayLength.ShouldBe(0u); } /// The Pascal-case snapshot a EF entity serialises to in the artifact