feat(config): DraftValidator rule + DraftSnapshot.VirtualTags for Tag/VirtualTag NodeId collisions

This commit is contained in:
Joseph Doherty
2026-06-07 10:33:45 -04:00
parent 6b36eff2d3
commit 83c7149be0
3 changed files with 98 additions and 0 deletions
@@ -186,6 +186,76 @@ public sealed class DraftValidatorTests
errors.ShouldContain(e => e.Code == "UnsSegmentInvalid");
}
// ------------------------------------------------------------------------------------
// ValidateNoEquipmentSignalNameCollision — Tag/VirtualTag NodeId collision
// ------------------------------------------------------------------------------------
/// <summary>Verifies that an equipment-bound Tag and a VirtualTag sharing the same
/// equipment+name collide on a single OPC UA NodeId.</summary>
[Fact]
public void Tag_and_VirtualTag_same_equipment_and_name_collide()
{
var draft = new DraftSnapshot
{
GenerationId = 1, ClusterId = "c",
Tags = [BuildTag(equipmentId: "eq-1", name: "speed", folderPath: null)],
VirtualTags = [BuildVirtualTag(equipmentId: "eq-1", name: "speed")],
};
DraftValidator.Validate(draft).ShouldContain(e => e.Code == "EquipmentSignalNameCollision");
}
/// <summary>Verifies that a Tag in a sub-folder does not collide with a VirtualTag at the
/// equipment root even when the names match — the NodeId keys differ.</summary>
[Fact]
public void Same_name_different_folder_does_not_collide()
{
var draft = new DraftSnapshot
{
GenerationId = 1, ClusterId = "c",
Tags = [BuildTag(equipmentId: "eq-1", name: "speed", folderPath: "metrics")],
VirtualTags = [BuildVirtualTag(equipmentId: "eq-1", name: "speed")],
};
DraftValidator.Validate(draft).ShouldNotContain(e => e.Code == "EquipmentSignalNameCollision");
}
/// <summary>Verifies that a SystemPlatform Tag (EquipmentId == null) is excluded from the
/// equipment-signal node space and so never collides with an equipment VirtualTag.</summary>
[Fact]
public void SystemPlatform_tag_sharing_name_with_equipment_vtag_excluded()
{
var draft = new DraftSnapshot
{
GenerationId = 1, ClusterId = "c",
Tags = [BuildTag(equipmentId: null, name: "speed", folderPath: null)],
VirtualTags = [BuildVirtualTag(equipmentId: "eq-1", name: "speed")],
};
DraftValidator.Validate(draft).ShouldNotContain(e => e.Code == "EquipmentSignalNameCollision");
}
private static Tag BuildTag(string? equipmentId, string name, string? folderPath) => new()
{
TagId = $"tag-{name}",
DriverInstanceId = "d",
EquipmentId = equipmentId,
Name = name,
FolderPath = folderPath,
DataType = "Float",
AccessLevel = TagAccessLevel.Read,
TagConfig = "{}",
};
private static VirtualTag BuildVirtualTag(string equipmentId, string name) => new()
{
VirtualTagId = $"vtag-{name}",
EquipmentId = equipmentId,
Name = name,
DataType = "Float",
ScriptId = "s-1",
};
// ------------------------------------------------------------------------------------
// Phase 6.3 task #148 part 2 — ValidateClusterTopology
// ------------------------------------------------------------------------------------