test(runtime): cover disabled-array + zero-length in artifact parity round-trip (review)

This commit is contained in:
Joseph Doherty
2026-06-16 21:45:19 -04:00
parent 0a747c343d
commit eb8a8dc19d
@@ -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);
}
/// <summary>The Pascal-case snapshot a <see cref="Tag"/> EF entity serialises to in the artifact