fix(adminui): converter skips name-collisions + disabled relays (review)

This commit is contained in:
Joseph Doherty
2026-06-11 21:44:19 -04:00
parent 2a30d7185a
commit d19271fff8
2 changed files with 151 additions and 5 deletions
@@ -116,7 +116,7 @@ public sealed class UnsTreeServiceRelayConverterTests
/// <summary>Adds a virtual tag bound to a script on an equipment.</summary>
private static void AddVirtualTag(
string dbName, string virtualTagId, string equipmentId, string name, string scriptId,
string dataType = "Int32", bool historize = false)
string dataType = "Int32", bool historize = false, bool enabled = true)
{
using var db = UnsTreeTestDb.CreateNamed(dbName);
db.VirtualTags.Add(new VirtualTag
@@ -127,6 +127,24 @@ public sealed class UnsTreeServiceRelayConverterTests
DataType = dataType,
ScriptId = scriptId,
Historize = historize,
Enabled = enabled,
});
db.SaveChanges();
}
/// <summary>Adds a plain (non-alias) Tag on an equipment, bound to the Modbus driver.</summary>
private static void AddPlainTag(string dbName, string tagId, string equipmentId, string name)
{
using var db = UnsTreeTestDb.CreateNamed(dbName);
db.Tags.Add(new Tag
{
TagId = tagId,
DriverInstanceId = ModbusDriverId,
EquipmentId = equipmentId,
Name = name,
DataType = "Int32",
AccessLevel = TagAccessLevel.Read,
TagConfig = "{}",
});
db.SaveChanges();
}
@@ -514,4 +532,83 @@ public sealed class UnsTreeServiceRelayConverterTests
db.VirtualTags.Any(v => v.VirtualTagId == "VT-1").ShouldBeTrue();
db.Tags.Any(t => t.Name == "speed").ShouldBeFalse();
}
// ----- Fix A: existing Tag name collision -> skip + batch continues -----
/// <summary>
/// When a relay VirtualTag has the same name as an existing Tag on the same equipment the
/// converter must skip it (not throw DbUpdateException from a unique-index violation) and continue
/// converting any other relay on the same equipment in the same batch. The colliding VirtualTag
/// and its Script must remain unchanged; exactly one Tag with that name must exist.
/// </summary>
[Fact]
public async Task Name_collision_with_existing_tag_is_skipped_and_batch_continues()
{
var dbName = SeedCluster();
AddEquipment(dbName, "EQ-1", "m_001");
// Existing plain Tag named "speed-rpm" (e.g. a manually-created Modbus tag).
AddPlainTag(dbName, "TAG-EXISTING", "EQ-1", "speed-rpm");
// Relay VirtualTag also named "speed-rpm" — would collide on the unique index.
AddScript(dbName, "SCRIPT-COLLIDE", Relay("TestMachine_020.SpeedRpm"));
AddVirtualTag(dbName, "VT-COLLIDE", "EQ-1", "speed-rpm", "SCRIPT-COLLIDE");
// A second, non-colliding relay on the same equipment in the same batch.
AddScript(dbName, "SCRIPT-OK", Relay("TestMachine_020.Torque"));
AddVirtualTag(dbName, "VT-OK", "EQ-1", "torque", "SCRIPT-OK");
var service = new UnsTreeService(UnsTreeTestDb.Factory(dbName));
var result = await service.ConvertRelayVirtualTagsToAliasesAsync("EQ-1", dryRun: false);
// The colliding vtag must appear in Skipped with a reason mentioning "already exists".
var skip = result.Skipped.ShouldHaveSingleItem(s => s.VirtualTagName == "speed-rpm");
skip.Reason.ShouldContain("already exists", Case.Insensitive);
// The non-colliding relay must still have been converted (batch was NOT aborted).
result.Converted.ShouldHaveSingleItem(i => i.VirtualTagName == "torque");
using var db = UnsTreeTestDb.CreateNamed(dbName);
// Exactly one Tag named "speed-rpm" (the original plain tag) — no duplicate was added.
db.Tags.Count(t => t.EquipmentId == "EQ-1" && t.Name == "speed-rpm").ShouldBe(1);
db.Tags.Single(t => t.EquipmentId == "EQ-1" && t.Name == "speed-rpm").TagId.ShouldBe("TAG-EXISTING");
// The colliding VirtualTag (and its script) must still be present.
db.VirtualTags.Any(v => v.VirtualTagId == "VT-COLLIDE").ShouldBeTrue();
db.Scripts.Any(s => s.ScriptId == "SCRIPT-COLLIDE").ShouldBeTrue();
// The non-colliding relay's Tag exists and its vtag + script are gone.
db.Tags.Any(t => t.EquipmentId == "EQ-1" && t.Name == "torque").ShouldBeTrue();
db.VirtualTags.Any(v => v.VirtualTagId == "VT-OK").ShouldBeFalse();
db.Scripts.Any(s => s.ScriptId == "SCRIPT-OK").ShouldBeFalse();
}
// ----- Fix B: disabled relay -> skip + report -----
/// <summary>
/// A relay VirtualTag with <c>Enabled == false</c> must be skipped rather than silently promoted
/// into an always-active alias Tag. The reason must mention "disabled". The VirtualTag and Script
/// remain unchanged and no Tag is added.
/// </summary>
[Fact]
public async Task Disabled_relay_is_skipped()
{
var dbName = SeedCluster();
AddEquipment(dbName, "EQ-1", "m_001");
AddScript(dbName, "SCRIPT-1", Relay("TestMachine_020.DisabledVal"));
AddVirtualTag(dbName, "VT-1", "EQ-1", "disabled-relay", "SCRIPT-1", enabled: false);
var service = new UnsTreeService(UnsTreeTestDb.Factory(dbName));
var result = await service.ConvertRelayVirtualTagsToAliasesAsync("EQ-1", dryRun: false);
result.Converted.ShouldBeEmpty();
var skip = result.Skipped.ShouldHaveSingleItem(s => s.VirtualTagName == "disabled-relay");
skip.Reason.ShouldContain("disabled", Case.Insensitive);
using var db = UnsTreeTestDb.CreateNamed(dbName);
db.VirtualTags.Any(v => v.VirtualTagId == "VT-1").ShouldBeTrue();
db.Scripts.Any(s => s.ScriptId == "SCRIPT-1").ShouldBeTrue();
db.Tags.Any(t => t.Name == "disabled-relay").ShouldBeFalse();
}
}