feat(opcua): EquipmentTagPlan IsArray/ArrayLength + composer ExtractTagArray + applier wire-in

This commit is contained in:
Joseph Doherty
2026-06-16 21:27:43 -04:00
parent 3172b7bdee
commit 71cc417182
4 changed files with 236 additions and 5 deletions
@@ -174,6 +174,42 @@ public sealed class Phase7PlannerTests
plan.RemovedEquipmentVirtualTags.ShouldBeEmpty();
}
/// <summary>An equipment Tag with the same id but a toggled <c>IsArray</c> flag (and otherwise
/// identical fields) must route to ChangedEquipmentTags. This pins that <c>IsArray</c> is part of
/// <see cref="EquipmentTagPlan.Equals"/> (record value-equality) so an array-flag-only deploy is not
/// a silent no-op at the diff/IsEmpty gate — same posture as the Historize flag.</summary>
[Fact]
public void Same_id_with_toggled_isarray_routes_to_ChangedEquipmentTags()
{
var prev = new Phase7CompositionResult(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
{
EquipmentTags = new[]
{
new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "Buffer", DataType: "Int16",
FullName: "40001", Writable: false, Alarm: null, IsArray: false, ArrayLength: null),
},
};
var next = new Phase7CompositionResult(
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
{
EquipmentTags = new[]
{
new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "Buffer", DataType: "Int16",
FullName: "40001", Writable: false, Alarm: null, IsArray: true, ArrayLength: 16),
},
};
var plan = Phase7Planner.Compute(prev, next);
plan.IsEmpty.ShouldBeFalse();
plan.ChangedEquipmentTags.Single().Previous.IsArray.ShouldBeFalse();
plan.ChangedEquipmentTags.Single().Current.IsArray.ShouldBeTrue();
plan.ChangedEquipmentTags.Single().Current.ArrayLength.ShouldBe((uint)16);
plan.AddedEquipmentTags.ShouldBeEmpty();
plan.RemovedEquipmentTags.ShouldBeEmpty();
}
/// <summary>Regression guard for structural equality on <see cref="EquipmentVirtualTagPlan.DependencyRefs"/>:
/// two snapshots containing the SAME VirtualTag built from SEPARATE list instances must diff to an empty plan
/// (IReadOnlyList equality is BY REFERENCE without the custom Equals override, so every VirtualTag with