diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/Phase7ApplierTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/Phase7ApplierTests.cs
index d3eccd8a..8d12249f 100644
--- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/Phase7ApplierTests.cs
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/Phase7ApplierTests.cs
@@ -1277,6 +1277,102 @@ public sealed class Phase7ApplierTests
sink.SurgicalCalls.ShouldBeEmpty();
}
+ /// F10b guard — a tag that ALREADY carries an alarm (Alarm is not null on BOTH previous
+ /// and current, identical alarm info) is NOT surgical-eligible even when only Writable changes.
+ /// The explicit Alarm is null guard in TagDeltaIsSurgicalEligible prevents a false
+ /// positive: without it the with { Writable = … } override (which does NOT touch Alarm)
+ /// would leave both sides with equal Alarm values and the delta would WRONGLY look surgical.
+ /// An alarm-bearing tag is a Part 9 condition node, not a plain value variable, so any change to it
+ /// must go through a full rebuild.
+ [Fact]
+ public void Changed_alarm_bearing_tag_writable_only_still_rebuilds()
+ {
+ var sink = new RecordingSink();
+ var applier = new Phase7Applier(sink, NullLogger.Instance);
+
+ // Both previous and current carry an identical, non-null alarm — the tag is an alarm-bearing node.
+ var alarm = new EquipmentTagAlarmInfo("OffNormalAlarm", 700);
+ var previous = CompositionWithTags(
+ new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "OverTemp", DataType: "Boolean",
+ FullName: "00001", Writable: false, Alarm: alarm));
+ // Only Writable flips; alarm is unchanged (and still non-null on both sides).
+ var next = CompositionWithTags(
+ new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "OverTemp", DataType: "Boolean",
+ FullName: "00001", Writable: true, Alarm: alarm));
+
+ var plan = Phase7Planner.Compute(previous, next);
+
+ // Guard the arrange: the planner sees a changed tag (Writable differs) and nothing else.
+ plan.ChangedEquipmentTags.Count.ShouldBe(1);
+ plan.AddedEquipmentTags.ShouldBeEmpty();
+ plan.RemovedEquipmentTags.ShouldBeEmpty();
+
+ var outcome = applier.Apply(plan);
+
+ // The alarm-bearing tag is NOT surgical-eligible — it must rebuild and make NO surgical call.
+ outcome.RebuildCalled.ShouldBeTrue();
+ sink.RebuildCalls.ShouldBe(1);
+ sink.SurgicalCalls.ShouldBeEmpty(); // NO surgical write on an alarm-bearing tag
+ }
+
+ /// F10b multi-delta — when a plan contains TWO distinct surgical-eligible tag deltas (plain
+ /// value variables, no alarm, both changing only Writable), the applier must apply BOTH in
+ /// place (two UpdateTagAttributes calls) without triggering any rebuild. Proves the surgical
+ /// path iterates all eligible deltas rather than stopping after the first.
+ [Fact]
+ public void Two_surgical_eligible_tag_deltas_both_apply_in_place_no_rebuild()
+ {
+ var sink = new RecordingSink();
+ var applier = new Phase7Applier(sink, NullLogger.Instance);
+
+ // Two distinct plain (no-alarm) tags, both will flip Writable — both are surgical-eligible.
+ var previous = new Phase7CompositionResult(
+ Array.Empty(), Array.Empty(), Array.Empty())
+ {
+ EquipmentTags = new[]
+ {
+ new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "Speed", DataType: "Float", FullName: "40001", Writable: false, Alarm: null),
+ new EquipmentTagPlan("tag-2", "eq-1", "drv", FolderPath: "", Name: "Pressure", DataType: "Float", FullName: "40002", Writable: false, Alarm: null),
+ },
+ };
+ // Only Writable flips on both tags; everything else is identical.
+ var next = new Phase7CompositionResult(
+ Array.Empty(), Array.Empty(), Array.Empty())
+ {
+ EquipmentTags = new[]
+ {
+ new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "Speed", DataType: "Float", FullName: "40001", Writable: true, Alarm: null),
+ new EquipmentTagPlan("tag-2", "eq-1", "drv", FolderPath: "", Name: "Pressure", DataType: "Float", FullName: "40002", Writable: true, Alarm: null),
+ },
+ };
+
+ var plan = Phase7Planner.Compute(previous, next);
+
+ // Guard the arrange: exactly two changed tags, nothing else.
+ plan.ChangedEquipmentTags.Count.ShouldBe(2);
+ plan.AddedEquipmentTags.ShouldBeEmpty();
+ plan.RemovedEquipmentTags.ShouldBeEmpty();
+ plan.AddedEquipment.ShouldBeEmpty();
+ plan.RemovedEquipment.ShouldBeEmpty();
+
+ var outcome = applier.Apply(plan);
+
+ // No rebuild — both deltas are surgical-eligible.
+ outcome.RebuildCalled.ShouldBeFalse();
+ sink.RebuildCalls.ShouldBe(0);
+
+ // Exactly two surgical calls — one per eligible delta.
+ sink.SurgicalCalls.Count.ShouldBe(2);
+
+ // Both expected node ids must appear (order is not guaranteed).
+ var nodeId1 = EquipmentNodeIds.Variable("eq-1", "", "Speed");
+ var nodeId2 = EquipmentNodeIds.Variable("eq-1", "", "Pressure");
+ sink.SurgicalCalls.ShouldContain(c => c.NodeId == nodeId1 && c.Writable);
+ sink.SurgicalCalls.ShouldContain(c => c.NodeId == nodeId2 && c.Writable);
+
+ outcome.ChangedNodes.ShouldBe(2);
+ }
+
/// F10b — a surgical-eligible tag delta MIXED with another change (here an added equipment)
/// must still rebuild: the rebuild is forced by the OTHER change. The surgical path is taken ONLY when
/// the tag deltas are the sole change. No surgical call is made (the rebuild materialises everything).