test(opcua): pin F10b boundary — vtag EquipmentId-change + multi-vtag-mixed still rebuild
This commit is contained in:
@@ -696,6 +696,9 @@ public sealed class Phase7ApplierTests
|
||||
new EquipmentVirtualTagPlan("vt-1", "eq-1", FolderPath: "", Name: "Efficiency", DataType: "Float64",
|
||||
Expression: "ctx.GetTag(\"a\")", DependencyRefs: new[] { "a" }));
|
||||
// Same node-relevant fields; only the dependency set differs.
|
||||
// Note: DependencyRefs normally tracks the Expression (derived by the ref extractor), so a
|
||||
// deps-only divergence is an edge case — e.g. the extractor logic changed between two deploys —
|
||||
// but it is still correctly node-irrelevant: the materialised OPC UA node is unchanged.
|
||||
var next = CompositionWithVirtualTags(
|
||||
new EquipmentVirtualTagPlan("vt-1", "eq-1", FolderPath: "", Name: "Efficiency", DataType: "Float64",
|
||||
Expression: "ctx.GetTag(\"a\")", DependencyRefs: new[] { "a", "b" }));
|
||||
@@ -861,6 +864,95 @@ public sealed class Phase7ApplierTests
|
||||
sink.RebuildCalls.ShouldBe(1);
|
||||
}
|
||||
|
||||
/// <summary>F10b safe-default — a vtag delta where the node-affecting <c>EquipmentId</c> changed
|
||||
/// (admin reassigns the vtag to a different equipment) must rebuild. The materialised node lives
|
||||
/// under the old equipment folder; moving it to another folder requires a full address-space
|
||||
/// rebuild. EquipmentId is NOT in the node-irrelevant whitelist (Expression/DependencyRefs/Historize),
|
||||
/// so the applier must not skip.</summary>
|
||||
[Fact]
|
||||
public void Changed_virtual_tag_equipment_id_triggers_rebuild()
|
||||
{
|
||||
var sink = new RecordingSink();
|
||||
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance);
|
||||
|
||||
var previous = CompositionWithVirtualTags(
|
||||
new EquipmentVirtualTagPlan("vt-1", "eq-1", FolderPath: "", Name: "Efficiency", DataType: "Float64",
|
||||
Expression: "ctx.GetTag(\"a\")", DependencyRefs: new[] { "a" }));
|
||||
// Same VirtualTagId, identical Expression/DependencyRefs/DataType/Name/FolderPath —
|
||||
// but reassigned to a different equipment ("eq-2" instead of "eq-1").
|
||||
var next = CompositionWithVirtualTags(
|
||||
new EquipmentVirtualTagPlan("vt-1", "eq-2", FolderPath: "", Name: "Efficiency", DataType: "Float64",
|
||||
Expression: "ctx.GetTag(\"a\")", DependencyRefs: new[] { "a" }));
|
||||
|
||||
var plan = Phase7Planner.Compute(previous, next);
|
||||
|
||||
// Guard the arrange: the planner sees this as a changed vtag (same id, different EquipmentId).
|
||||
plan.ChangedEquipmentVirtualTags.Count.ShouldBe(1);
|
||||
plan.AddedEquipmentVirtualTags.ShouldBeEmpty();
|
||||
plan.RemovedEquipmentVirtualTags.ShouldBeEmpty();
|
||||
|
||||
var outcome = applier.Apply(plan);
|
||||
|
||||
// EquipmentId is node-affecting — the node moves to a different folder — so rebuild is required.
|
||||
outcome.RebuildCalled.ShouldBeTrue();
|
||||
sink.RebuildCalls.ShouldBe(1);
|
||||
outcome.ChangedNodes.ShouldBe(1);
|
||||
}
|
||||
|
||||
/// <summary>F10b safe-default — when a plan contains TWO changed vtags and ONE is node-irrelevant
|
||||
/// (Expression-only) while the OTHER is structural (DataType changed), the applier must still rebuild.
|
||||
/// The <c>Any(d => !VtagDeltaIsNodeIrrelevant(d))</c> predicate must catch the structural delta
|
||||
/// even though its sibling vtag would individually qualify for the skip.</summary>
|
||||
[Fact]
|
||||
public void Changed_virtual_tags_one_irrelevant_one_structural_triggers_rebuild()
|
||||
{
|
||||
var sink = new RecordingSink();
|
||||
var applier = new Phase7Applier(sink, NullLogger<Phase7Applier>.Instance);
|
||||
|
||||
var previous = new Phase7CompositionResult(
|
||||
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
|
||||
{
|
||||
EquipmentVirtualTags = new[]
|
||||
{
|
||||
// vt-A: will receive an Expression-only edit (node-irrelevant).
|
||||
new EquipmentVirtualTagPlan("vt-a", "eq-1", FolderPath: "", Name: "SpeedRpm", DataType: "Float64",
|
||||
Expression: "ctx.GetTag(\"speed\") * 60", DependencyRefs: new[] { "speed" }),
|
||||
// vt-B: will receive a DataType change (structural / node-affecting).
|
||||
new EquipmentVirtualTagPlan("vt-b", "eq-1", FolderPath: "", Name: "LoadPct", DataType: "Float64",
|
||||
Expression: "ctx.GetTag(\"load\")", DependencyRefs: new[] { "load" }),
|
||||
},
|
||||
};
|
||||
var next = new Phase7CompositionResult(
|
||||
Array.Empty<EquipmentNode>(), Array.Empty<DriverInstancePlan>(), Array.Empty<ScriptedAlarmPlan>())
|
||||
{
|
||||
EquipmentVirtualTags = new[]
|
||||
{
|
||||
// vt-A: Expression-only change — individually node-irrelevant.
|
||||
new EquipmentVirtualTagPlan("vt-a", "eq-1", FolderPath: "", Name: "SpeedRpm", DataType: "Float64",
|
||||
Expression: "ctx.GetTag(\"speed\") * 120", DependencyRefs: new[] { "speed" }),
|
||||
// vt-B: DataType flipped — structurally node-affecting.
|
||||
new EquipmentVirtualTagPlan("vt-b", "eq-1", FolderPath: "", Name: "LoadPct", DataType: "Int32",
|
||||
Expression: "ctx.GetTag(\"load\")", DependencyRefs: new[] { "load" }),
|
||||
},
|
||||
};
|
||||
|
||||
var plan = Phase7Planner.Compute(previous, next);
|
||||
|
||||
// Guard the arrange: both vtags are diffed as changed, nothing else.
|
||||
plan.ChangedEquipmentVirtualTags.Count.ShouldBe(2);
|
||||
plan.AddedEquipmentVirtualTags.ShouldBeEmpty();
|
||||
plan.RemovedEquipmentVirtualTags.ShouldBeEmpty();
|
||||
plan.ChangedEquipmentTags.ShouldBeEmpty();
|
||||
plan.ChangedEquipment.ShouldBeEmpty();
|
||||
|
||||
var outcome = applier.Apply(plan);
|
||||
|
||||
// The structural delta on vt-B must force a rebuild even though vt-A is node-irrelevant.
|
||||
outcome.RebuildCalled.ShouldBeTrue();
|
||||
sink.RebuildCalls.ShouldBe(1);
|
||||
outcome.ChangedNodes.ShouldBe(2);
|
||||
}
|
||||
|
||||
/// <summary>H1a — a deploy that ONLY edits an existing scripted alarm (here its message template)
|
||||
/// must rebuild the address space. The planner diffs the thin <c>ScriptedAlarmPlan</c> projection
|
||||
/// into <c>ChangedAlarms</c> alone; the applier must drive exactly one rebuild so the condition node
|
||||
|
||||
Reference in New Issue
Block a user