feat(opcuaserver): classifier — non-alarm raw-tag + UNS-reference deltas are surgical, not rebuilds
This commit is contained in:
+121
-6
@@ -6,8 +6,11 @@ namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// v3 Batch 4 (B4-WP1) — the classifier folds the Raw + UNS subtree diff sets into its routing policy:
|
||||
/// raw/uns adds count as adds (PureAdd), removes as removes (PureRemove), and any Changed raw/uns delta
|
||||
/// routes to Rebuild (the safe default — WP3's applier owns surgical refinement).
|
||||
/// raw/uns adds count as adds (PureAdd), removes as removes (PureRemove).
|
||||
/// <para>Surgical deploys (2026-08-15 Task 4) narrowed the Changed sets: a NON-alarm raw-tag delta whose
|
||||
/// only differences are the in-place-updatable attributes and/or its <c>ReferencingEquipmentPaths</c> is
|
||||
/// surgical (NOT a rebuild), and every UNS-reference delta is applied remove+recreate. Alarm-bearing and
|
||||
/// identity-field raw-tag deltas still route to Rebuild — the default-closed WP4 notifier guard.</para>
|
||||
/// </summary>
|
||||
public sealed class AddressSpaceChangeClassifierDualNamespaceTests
|
||||
{
|
||||
@@ -71,8 +74,12 @@ public sealed class AddressSpaceChangeClassifierDualNamespaceTests
|
||||
AddressSpaceChangeClassifier.Classify(plan).ShouldBe(AddressSpaceChangeKind.AddRemoveMix);
|
||||
}
|
||||
|
||||
/// <summary>The headline surgical case: adding/dropping a <c>UnsTagReference</c> flips the BACKING raw tag
|
||||
/// to Changed (ReferencingEquipmentPaths is part of RawTagPlan's equality). For a non-alarm VALUE tag that
|
||||
/// set is node-inert — it only drives native-alarm notifier wiring — so it must NOT tear down the world.
|
||||
/// (Was <c>Changed_raw_tag_is_Rebuild</c>; deliberately re-pointed by 2026-08-15 Task 4.)</summary>
|
||||
[Fact]
|
||||
public void Changed_raw_tag_is_Rebuild()
|
||||
public void Reference_only_raw_tag_delta_is_not_a_rebuild()
|
||||
{
|
||||
var plan = Empty() with
|
||||
{
|
||||
@@ -80,14 +87,88 @@ public sealed class AddressSpaceChangeClassifierDualNamespaceTests
|
||||
{
|
||||
new AddressSpacePlan.RawTagDelta(
|
||||
RawTag("Plant/Modbus1/PLC-A/Speed"),
|
||||
RawTag("Plant/Modbus1/PLC-A/Speed") with { IsHistorized = true }),
|
||||
RawTag("Plant/Modbus1/PLC-A/Speed") with
|
||||
{
|
||||
ReferencingEquipmentPaths = new[] { "filling/line-1/station-1" },
|
||||
}),
|
||||
},
|
||||
};
|
||||
AddressSpaceChangeClassifier.Classify(plan).ShouldBe(AddressSpaceChangeKind.AttributeOnly);
|
||||
}
|
||||
|
||||
/// <summary>The in-place-updatable attribute whitelist (Writable / IsHistorized / HistorianTagname /
|
||||
/// DataType / IsArray / ArrayLength) on a non-alarm raw tag is surgical, not a rebuild.</summary>
|
||||
[Theory]
|
||||
[InlineData("writable")]
|
||||
[InlineData("historized")]
|
||||
[InlineData("historianTagname")]
|
||||
[InlineData("dataType")]
|
||||
[InlineData("array")]
|
||||
public void Attribute_raw_tag_delta_is_not_a_rebuild(string field)
|
||||
{
|
||||
var prev = RawTag("Plant/Modbus1/PLC-A/Speed");
|
||||
var cur = field switch
|
||||
{
|
||||
"writable" => prev with { Writable = true },
|
||||
"historized" => prev with { IsHistorized = true },
|
||||
"historianTagname" => prev with { IsHistorized = true, HistorianTagname = "Plant.PLC_A.Speed" },
|
||||
"dataType" => prev with { DataType = "Double" },
|
||||
"array" => prev with { IsArray = true, ArrayLength = 8 },
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(field)),
|
||||
};
|
||||
|
||||
var plan = Empty() with { ChangedRawTags = new[] { new AddressSpacePlan.RawTagDelta(prev, cur) } };
|
||||
AddressSpaceChangeClassifier.Classify(plan).ShouldBe(AddressSpaceChangeKind.AttributeOnly);
|
||||
}
|
||||
|
||||
/// <summary>WP4 binding guard: a raw tag carrying a native alarm on EITHER side still rebuilds — the
|
||||
/// rebuild is what clears + re-wires the per-equipment alarm notifiers.</summary>
|
||||
[Theory]
|
||||
[InlineData(true, false)]
|
||||
[InlineData(false, true)]
|
||||
[InlineData(true, true)]
|
||||
public void Alarm_bearing_raw_tag_delta_still_rebuilds(bool alarmBefore, bool alarmAfter)
|
||||
{
|
||||
var alarm = new EquipmentTagAlarmInfo("OffNormalAlarm", 700);
|
||||
var prev = RawTag("Plant/Modbus1/PLC-A/Speed") with { Alarm = alarmBefore ? alarm : null };
|
||||
var cur = RawTag("Plant/Modbus1/PLC-A/Speed") with
|
||||
{
|
||||
Alarm = alarmAfter ? alarm : null,
|
||||
ReferencingEquipmentPaths = new[] { "filling/line-1/station-1" },
|
||||
};
|
||||
|
||||
var plan = Empty() with { ChangedRawTags = new[] { new AddressSpacePlan.RawTagDelta(prev, cur) } };
|
||||
AddressSpaceChangeClassifier.Classify(plan).ShouldBe(AddressSpaceChangeKind.Rebuild);
|
||||
}
|
||||
|
||||
/// <summary>Default-closed: an identity field outside the whitelist (DriverInstanceId re-routes the node to
|
||||
/// another driver point; Name/TagId/ParentNodeId re-shape identity) still forces a full rebuild.</summary>
|
||||
[Theory]
|
||||
[InlineData("driver")]
|
||||
[InlineData("name")]
|
||||
[InlineData("tagId")]
|
||||
[InlineData("parent")]
|
||||
public void Identity_field_raw_tag_delta_still_rebuilds(string field)
|
||||
{
|
||||
var prev = RawTag("Plant/Modbus1/PLC-A/Speed");
|
||||
var cur = field switch
|
||||
{
|
||||
"driver" => prev with { DriverInstanceId = "drv-2" },
|
||||
"name" => prev with { Name = "Velocity" },
|
||||
"tagId" => prev with { TagId = "t-2" },
|
||||
"parent" => prev with { ParentNodeId = "Plant/Modbus1/PLC-B" },
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(field)),
|
||||
};
|
||||
|
||||
var plan = Empty() with { ChangedRawTags = new[] { new AddressSpacePlan.RawTagDelta(prev, cur) } };
|
||||
AddressSpaceChangeClassifier.Classify(plan).ShouldBe(AddressSpaceChangeKind.Rebuild);
|
||||
}
|
||||
|
||||
/// <summary>A UNS-reference re-point (BackingRawPath moved) is applied surgically (remove-previous +
|
||||
/// recreate-current), NOT as a rebuild. (Was <c>Uns_reference_repoint_is_Rebuild</c>; deliberately
|
||||
/// re-pointed by 2026-08-15 Task 4.)</summary>
|
||||
[Fact]
|
||||
public void Uns_reference_repoint_is_Rebuild()
|
||||
public void Uns_reference_repoint_delta_is_not_a_rebuild()
|
||||
{
|
||||
var plan = Empty() with
|
||||
{
|
||||
@@ -98,6 +179,40 @@ public sealed class AddressSpaceChangeClassifierDualNamespaceTests
|
||||
UnsRef("filling/line-1/station-1/Speed") with { BackingRawPath = "Plant/Modbus1/PLC-A/Velocity" }),
|
||||
},
|
||||
};
|
||||
AddressSpaceChangeClassifier.Classify(plan).ShouldBe(AddressSpaceChangeKind.Rebuild);
|
||||
AddressSpaceChangeClassifier.Classify(plan).ShouldBe(AddressSpaceChangeKind.AttributeOnly);
|
||||
}
|
||||
|
||||
/// <summary>The other UnsReferenceDelta variant — a display-name override edit keeps the row id while the
|
||||
/// NodeId + EffectiveName change. Also surgical (remove-previous + recreate-current).</summary>
|
||||
[Fact]
|
||||
public void Uns_reference_display_name_delta_is_not_a_rebuild()
|
||||
{
|
||||
var plan = Empty() with
|
||||
{
|
||||
ChangedUnsReferenceVariables = new[]
|
||||
{
|
||||
new AddressSpacePlan.UnsReferenceDelta(
|
||||
UnsRef("filling/line-1/station-1/Speed"),
|
||||
UnsRef("filling/line-1/station-1/LineSpeed") with { EffectiveName = "LineSpeed" }),
|
||||
},
|
||||
};
|
||||
AddressSpaceChangeClassifier.Classify(plan).ShouldBe(AddressSpaceChangeKind.AttributeOnly);
|
||||
}
|
||||
|
||||
/// <summary>The headline end-to-end shape: adding ONE UNS reference produces an added UNS variable PLUS the
|
||||
/// induced reference-only delta on the backing raw tag. That must classify PureAdd — one node created,
|
||||
/// zero teardown, every client subscription server-wide survives.</summary>
|
||||
[Fact]
|
||||
public void Uns_reference_add_with_induced_reference_only_raw_delta_is_pure_add()
|
||||
{
|
||||
var prev = RawTag("Plant/Modbus1/PLC-A/Speed");
|
||||
var cur = prev with { ReferencingEquipmentPaths = new[] { "filling/line-1/station-1" } };
|
||||
|
||||
var plan = Empty() with
|
||||
{
|
||||
AddedUnsReferenceVariables = new[] { UnsRef("filling/line-1/station-1/Speed") },
|
||||
ChangedRawTags = new[] { new AddressSpacePlan.RawTagDelta(prev, cur) },
|
||||
};
|
||||
AddressSpaceChangeClassifier.Classify(plan).ShouldBe(AddressSpaceChangeKind.PureAdd);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user