diff --git a/docs/plans/2026-06-06-equipment-namespace-structure-milestone.md.tasks.json b/docs/plans/2026-06-06-equipment-namespace-structure-milestone.md.tasks.json index 17476ca4..62ad8929 100644 --- a/docs/plans/2026-06-06-equipment-namespace-structure-milestone.md.tasks.json +++ b/docs/plans/2026-06-06-equipment-namespace-structure-milestone.md.tasks.json @@ -6,7 +6,7 @@ {"id": 0, "nativeTaskId": 86, "subject": "Task 0: Confirm signatures + record architecture decisions", "status": "completed", "blockedBy": []}, {"id": 1, "nativeTaskId": 87, "subject": "Task 1: Carry equipment signals in the composition + artifact", "status": "completed", "blockedBy": [86]}, {"id": 2, "nativeTaskId": 88, "subject": "Task 2: Materialise equipment signals in the live rebuild", "status": "completed", "blockedBy": [87]}, - {"id": 3, "nativeTaskId": 89, "subject": "Task 3: Friendly browse names for UNS folders", "status": "pending", "blockedBy": [87]}, + {"id": 3, "nativeTaskId": 89, "subject": "Task 3: Friendly browse names for UNS folders", "status": "completed", "blockedBy": [87]}, {"id": 4, "nativeTaskId": 90, "subject": "Task 4: Idempotency + restart-safety", "status": "pending", "blockedBy": [88]}, {"id": 5, "nativeTaskId": 91, "subject": "Task 5: docker-dev integration verification + tool support", "status": "pending", "blockedBy": [88, 89]} ], diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/Phase7Composer.cs b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/Phase7Composer.cs index e998fc88..d22edc2f 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/Phase7Composer.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/Phase7Composer.cs @@ -169,7 +169,10 @@ public static class Phase7Composer var nodes = equipment .OrderBy(e => e.EquipmentId, StringComparer.Ordinal) - .Select(e => new EquipmentNode(e.EquipmentId, e.MachineCode, e.UnsLineId)) + // DisplayName = the UNS level-5 Name segment (friendly browse name, matching the Area + // and Line projections + EquipmentNodeWalker) — NOT the colloquial MachineCode. NodeId + // stays the logical EquipmentId so browse-path resolution + ACLs are unaffected. + .Select(e => new EquipmentNode(e.EquipmentId, e.Name, e.UnsLineId)) .ToList(); var plans = driverInstances diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DeploymentArtifact.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DeploymentArtifact.cs index 31f48120..38fc5b39 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DeploymentArtifact.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DeploymentArtifact.cs @@ -365,7 +365,10 @@ public static class DeploymentArtifact private static EquipmentNode? ReadEquipmentNode(JsonElement el) { var id = el.TryGetProperty("EquipmentId", out var idEl) ? idEl.GetString() : null; - var displayName = el.TryGetProperty("MachineCode", out var mcEl) ? mcEl.GetString() : null; + // DisplayName = the UNS level-5 Name segment (friendly browse name, matching UnsArea/UnsLine + // + the live rebuild's source of truth) — NOT the colloquial MachineCode. NodeId stays the + // logical EquipmentId. + var displayName = el.TryGetProperty("Name", out var nameEl) ? nameEl.GetString() : null; var lineId = el.TryGetProperty("UnsLineId", out var lineEl) ? lineEl.GetString() : null; if (string.IsNullOrWhiteSpace(id)) return null; return new EquipmentNode(id!, displayName ?? id!, lineId ?? string.Empty); diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/Phase7ComposerPurityTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/Phase7ComposerPurityTests.cs index 60d8e57e..e78a23bc 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/Phase7ComposerPurityTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/Phase7ComposerPurityTests.cs @@ -75,6 +75,22 @@ public sealed class Phase7ComposerPurityTests .ShouldBe(new[] { "a", "m", "z" }); } + /// Verifies UNS equipment folders browse by the friendly UNS Name segment + /// (not the colloquial MachineCode, not the logical EquipmentId) while the NodeId stays the + /// logical EquipmentId — so browse-path resolution + ACLs are unaffected (decision #3). + [Fact] + public void Equipment_node_DisplayName_is_the_UNS_Name_not_MachineCode() + { + var equipment = new[] { NewEquipment("filling-eq") }; // Name="filling-eq", MachineCode="FILLING-EQ" + + var node = Phase7Composer.Compose(equipment, Array.Empty(), Array.Empty()) + .EquipmentNodes.ShouldHaveSingleItem(); + + node.EquipmentId.ShouldBe("filling-eq"); // NodeId stays the logical Id + node.DisplayName.ShouldBe("filling-eq"); // browse name = UNS Name segment + node.DisplayName.ShouldNotBe("FILLING-EQ"); // not the colloquial MachineCode + } + private static Equipment NewEquipment(string id) => new() { EquipmentId = id, diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DeploymentArtifactTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DeploymentArtifactTests.cs index 22f5ffb1..a6ed7bd9 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DeploymentArtifactTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DeploymentArtifactTests.cs @@ -177,6 +177,28 @@ public sealed class DeploymentArtifactTests c.GalaxyTags.ShouldContain(g => g.TagId == "tag-gx"); } + /// + /// Verifies ParseComposition sets the equipment folder DisplayName to the UNS Name + /// segment — the source the live rebuild actually uses — not the colloquial MachineCode, so + /// equipment browses by its friendly UNS name. NodeId stays the logical EquipmentId. + /// + [Fact] + public void ParseComposition_equipment_DisplayName_is_UNS_Name_not_MachineCode() + { + var blob = JsonSerializer.SerializeToUtf8Bytes(new + { + Equipment = new[] + { + new { EquipmentId = "eq-1", Name = "filling-eq", MachineCode = "FILLING-EQ", UnsLineId = "line-1" }, + }, + }); + + var node = DeploymentArtifact.ParseComposition(blob).EquipmentNodes.ShouldHaveSingleItem(); + + node.EquipmentId.ShouldBe("eq-1"); + node.DisplayName.ShouldBe("filling-eq"); + } + /// Verifies that specs missing required fields are dropped. [Fact] public void Spec_missing_required_fields_is_dropped()