diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/IUnsTreeService.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/IUnsTreeService.cs
index f75486f6..3bff22e0 100644
--- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/IUnsTreeService.cs
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/IUnsTreeService.cs
@@ -375,7 +375,8 @@ public interface IUnsTreeService
/// The owning equipment.
/// The operator-editable virtual-tag fields.
/// A token to cancel the operation.
- /// Success, or one of the guard failures.
+ /// Success, or one of the guard failures; or if the chosen script uses the {{equip}}
+ /// token but the equipment has no derivable single tag base.
Task CreateVirtualTagAsync(string equipmentId, VirtualTagInput input, CancellationToken ct = default);
///
@@ -389,7 +390,8 @@ public interface IUnsTreeService
/// The new operator-editable virtual-tag fields.
/// The concurrency token the caller last read.
/// A token to cancel the operation.
- /// Success, a missing-row failure, a guard failure, or a concurrency failure.
+ /// Success, a missing-row failure, a guard failure, or a concurrency failure; or if the
+ /// chosen script uses the {{equip}} token but the equipment has no derivable single tag base.
Task UpdateVirtualTagAsync(string virtualTagId, VirtualTagInput input, byte[] rowVersion, CancellationToken ct = default);
///
diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/UnsTreeService.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/UnsTreeService.cs
index f719f16f..8632ba13 100644
--- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/UnsTreeService.cs
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/UnsTreeService.cs
@@ -908,12 +908,10 @@ public sealed class UnsTreeService(IDbContextFactory dbF
var fullNames = configs.Select(TagConfigFullName.Extract);
if (EquipmentScriptPaths.DeriveEquipmentBase(fullNames) is null)
{
- // NOTE: literal {{equip}} must survive — in a C# interpolated string `{{`/`}}`
- // collapse to single braces, so keep the token text in a NON-interpolated segment.
return new UnsMutationResult(false,
$"Equipment '{equipmentId}' has no single tag base, so the "
- + "{{equip}} token can't be resolved. Add at least one driver tag under this "
- + "equipment (all sharing one object prefix), or remove {{equip}} from the script.");
+ + $"{EquipmentScriptPaths.EquipToken} token can't be resolved. Add at least one driver tag under this "
+ + $"equipment (all sharing one object prefix), or remove {EquipmentScriptPaths.EquipToken} from the script.");
}
return null;
}
diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/VirtualTagEquipTokenValidationTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/VirtualTagEquipTokenValidationTests.cs
index 5fff5e3b..58922c56 100644
--- a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/VirtualTagEquipTokenValidationTests.cs
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/VirtualTagEquipTokenValidationTests.cs
@@ -138,6 +138,41 @@ public sealed class VirtualTagEquipTokenValidationTests
result.Error.ShouldBeNull();
}
+ ///
+ /// {{equip}} script + TWO driver tags whose FullNames have DIFFERENT object prefixes
+ /// (no single base can be derived) → create rejected, error names equipment + token.
+ ///
+ [Fact]
+ public async Task Create_equip_token_with_divergent_prefixes_rejected()
+ {
+ var (service, dbName) = Fresh();
+ Seed(dbName, EquipBaseScript, tagFullName: "TestMachine_001.X");
+ using (var db = UnsTreeTestDb.CreateNamed(dbName))
+ {
+ db.Tags.Add(new Tag
+ {
+ TagId = "TAG-2",
+ DriverInstanceId = "DRV-1",
+ EquipmentId = "EQ-1",
+ Name = "y",
+ DataType = "Float",
+ AccessLevel = TagAccessLevel.Read,
+ TagConfig = "{\"FullName\":\"DelmiaReceiver_001.Y\"}",
+ });
+ db.SaveChanges();
+ }
+
+ var result = await service.CreateVirtualTagAsync("EQ-1", Input());
+
+ result.Ok.ShouldBeFalse();
+ result.Error.ShouldNotBeNull();
+ result.Error.ShouldContain("EQ-1");
+ result.Error.ShouldContain("{{equip}}");
+
+ using var verifyDb = UnsTreeTestDb.CreateNamed(dbName);
+ verifyDb.VirtualTags.Any(v => v.VirtualTagId == "VTAG-1").ShouldBeFalse();
+ }
+
// ----- Update -----
/// {{equip}} script + a derivable base → update succeeds.