diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DeploymentArtifactAliasParityTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DeploymentArtifactAliasParityTests.cs
index 61bafad2..feb93c79 100644
--- a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DeploymentArtifactAliasParityTests.cs
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DeploymentArtifactAliasParityTests.cs
@@ -1,6 +1,10 @@
+using System.Linq;
using System.Text.Json;
using Shouldly;
using Xunit;
+using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
+using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
+using ZB.MOM.WW.OtOpcUa.OpcUaServer;
using ZB.MOM.WW.OtOpcUa.Runtime.Drivers;
namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Drivers;
@@ -96,4 +100,164 @@ public sealed class DeploymentArtifactAliasParityTests
c.EquipmentTags.ShouldBeEmpty();
}
+
+ ///
+ /// The load-bearing direct byte-parity proof for the two equipment-tag producers: for the SAME
+ /// input draft, the live-edit composer () and the
+ /// artifact decoder ()
+ /// must emit IDENTICAL EquipmentTags — element-wise equal on every field
+ /// (TagId, EquipmentId, DriverInstanceId, FolderPath, Name, DataType, FullName) AND in the same
+ /// ORDER. The draft holds a Galaxy equipment tag (EquipmentId set, GalaxyMxGateway driver in an
+ /// Equipment-kind namespace, TagConfig {"FullName":"DelmiaReceiver_001.DownloadPath"})
+ /// PLUS a Modbus equipment tag, so the parity is proven both for Galaxy specifically and across
+ /// drivers — Galaxy is now an ordinary Equipment-kind driver with no exception clause on either
+ /// side. Both sides sort by EquipmentId → FolderPath → Name (Ordinal), so identical input yields
+ /// identical order. EquipmentTagPlan is a plain positional record (all string members), so
+ /// SequenceEqual is full value-and-order equality.
+ ///
+ [Fact]
+ public void Composer_and_artifact_agree_on_galaxy_equipment_tag()
+ {
+ // ---- The ONE input draft both producers consume ----
+ // Equipment-kind namespace shared by both drivers (the qualifying gate on both sides).
+ var ns = new Namespace
+ {
+ NamespaceId = "ns-eq",
+ ClusterId = "c1",
+ Kind = NamespaceKind.Equipment,
+ NamespaceUri = "urn:eq",
+ };
+ // Galaxy is a standard Equipment-kind driver now — no driver-type exception on either side.
+ var galaxyDriver = new DriverInstance
+ {
+ DriverInstanceId = "drv-galaxy",
+ ClusterId = "c1",
+ NamespaceId = "ns-eq",
+ Name = "Galaxy1",
+ DriverType = "GalaxyMxGateway",
+ DriverConfig = "{}",
+ };
+ var modbusDriver = new DriverInstance
+ {
+ DriverInstanceId = "drv-modbus",
+ ClusterId = "c1",
+ NamespaceId = "ns-eq",
+ Name = "Modbus1",
+ DriverType = "Modbus",
+ DriverConfig = "{}",
+ };
+ var area = new UnsArea { UnsAreaId = "area-1", ClusterId = "c1", Name = "filling" };
+ var line = new UnsLine { UnsLineId = "line-1", UnsAreaId = "area-1", Name = "line-1" };
+ var galaxyEquip = new Equipment
+ {
+ EquipmentId = "eq-galaxy",
+ DriverInstanceId = "drv-galaxy",
+ UnsLineId = "line-1",
+ Name = "DelmiaReceiver",
+ MachineCode = "DELMIARECEIVER",
+ };
+ var modbusEquip = new Equipment
+ {
+ EquipmentId = "eq-modbus",
+ DriverInstanceId = "drv-modbus",
+ UnsLineId = "line-1",
+ Name = "FillingPump",
+ MachineCode = "FILLINGPUMP",
+ };
+
+ // The Galaxy equipment tag — FullName is the Galaxy ref "tag_name.AttributeName".
+ var galaxyTag = new Tag
+ {
+ TagId = "tag-galaxy",
+ DriverInstanceId = "drv-galaxy",
+ EquipmentId = "eq-galaxy",
+ FolderPath = null, // coalesces to "" on both sides
+ Name = "DownloadPath",
+ DataType = "String",
+ AccessLevel = TagAccessLevel.Read,
+ TagConfig = "{\"FullName\":\"DelmiaReceiver_001.DownloadPath\"}",
+ };
+ // A non-Galaxy (Modbus) equipment tag — proves the parity holds across drivers, not just Galaxy.
+ var modbusTag = new Tag
+ {
+ TagId = "tag-modbus",
+ DriverInstanceId = "drv-modbus",
+ EquipmentId = "eq-modbus",
+ FolderPath = "registers",
+ Name = "Speed",
+ DataType = "Float",
+ AccessLevel = TagAccessLevel.ReadWrite,
+ TagConfig = "{\"FullName\":\"40001\"}",
+ };
+
+ var areas = new[] { area };
+ var lines = new[] { line };
+ var equipment = new[] { galaxyEquip, modbusEquip };
+ var drivers = new[] { galaxyDriver, modbusDriver };
+ var tags = new[] { galaxyTag, modbusTag };
+ var namespaces = new[] { ns };
+
+ // ---- Side 1: the live-edit composer ----
+ var composed = Phase7Composer.Compose(
+ areas, lines, equipment, drivers, Array.Empty(), tags, namespaces);
+
+ // ---- Side 2: serialise the SAME draft to the artifact blob shape ConfigComposer emits
+ // (Pascal-case off the EF entities), then decode it. ----
+ var blob = JsonSerializer.SerializeToUtf8Bytes(new
+ {
+ Namespaces = new[]
+ {
+ new { ns.NamespaceId, ns.ClusterId, Kind = (int)ns.Kind },
+ },
+ DriverInstances = new[]
+ {
+ new { galaxyDriver.DriverInstanceId, galaxyDriver.DriverType, galaxyDriver.DriverConfig, galaxyDriver.NamespaceId, galaxyDriver.ClusterId },
+ new { modbusDriver.DriverInstanceId, modbusDriver.DriverType, modbusDriver.DriverConfig, modbusDriver.NamespaceId, modbusDriver.ClusterId },
+ },
+ Tags = new[]
+ {
+ ToSnapshot(galaxyTag),
+ ToSnapshot(modbusTag),
+ },
+ });
+
+ var decoded = DeploymentArtifact.ParseComposition(blob);
+
+ // ---- The equality contract: element-wise equal on ALL fields + same order ----
+ decoded.EquipmentTags.Count.ShouldBe(2);
+ // SequenceEqual = full value-and-order equality (EquipmentTagPlan is a positional record).
+ decoded.EquipmentTags.SequenceEqual(composed.EquipmentTags).ShouldBeTrue();
+
+ // Spell the per-field contract out too (so a future divergence names the offending field
+ // rather than just "sequences differ"), and pin the Galaxy tag's wire-level FullName.
+ foreach (var (d, x) in decoded.EquipmentTags.Zip(composed.EquipmentTags))
+ {
+ d.TagId.ShouldBe(x.TagId);
+ d.EquipmentId.ShouldBe(x.EquipmentId);
+ d.DriverInstanceId.ShouldBe(x.DriverInstanceId);
+ d.FolderPath.ShouldBe(x.FolderPath);
+ d.Name.ShouldBe(x.Name);
+ d.DataType.ShouldBe(x.DataType);
+ d.FullName.ShouldBe(x.FullName);
+ }
+
+ var galaxyPlan = decoded.EquipmentTags.Single(t => t.TagId == "tag-galaxy");
+ galaxyPlan.FullName.ShouldBe("DelmiaReceiver_001.DownloadPath");
+ galaxyPlan.EquipmentId.ShouldBe("eq-galaxy");
+ galaxyPlan.DriverInstanceId.ShouldBe("drv-galaxy");
+ galaxyPlan.FolderPath.ShouldBe(string.Empty); // null FolderPath coalesced identically on both sides
+ }
+
+ /// The full Pascal-case snapshot a EF entity serialises to in the
+ /// artifact (matches ConfigComposer): the equipment-tag decoder reads exactly these fields.
+ private static object ToSnapshot(Tag t) => new
+ {
+ t.TagId,
+ t.DriverInstanceId,
+ t.EquipmentId,
+ t.Name,
+ t.FolderPath,
+ t.DataType,
+ t.TagConfig,
+ };
}