fix(code-review): resolve OpcUaServer-001 — UNS Area/Line rename refreshes folder DisplayName
A rename-only deploy produced an IsEmpty plan that short-circuited before MaterialiseHierarchy, leaving the OPC UA folder DisplayName stale. AddressSpacePlanner now diffs UnsAreas/UnsLines by stable id into a RenamedFolders set (counted in IsEmpty); the applier refreshes the folder in place via a new UpdateFolderDisplayName on ISurgicalAddressSpaceSink (forwarded through DeferredAddressSpaceSink so it is NOT inert on driver hosts; falls back to rebuild when the sink is non-surgical). DeploymentArtifact byte-parity untouched (rename rides the existing Name round-trip). No EF migration, no serialized wire/proto contract change. +13 OpcUaServer tests, Runtime rebuild test.
This commit is contained in:
@@ -7,7 +7,7 @@
|
||||
| Review date | 2026-06-19 |
|
||||
| Commit reviewed | `7286d320` |
|
||||
| Status | Reviewed |
|
||||
| Open findings | 1 |
|
||||
| Open findings | 0 |
|
||||
|
||||
## Checklist coverage
|
||||
|
||||
@@ -40,7 +40,7 @@ a category produced nothing rather than leaving it blank.
|
||||
| Severity | Medium |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Location | `AddressSpacePlan.cs:56` (`AddressSpacePlan.IsEmpty`), `AddressSpacePlan.cs:80` (`AddressSpacePlanner.Compute`) |
|
||||
| Status | Open |
|
||||
| Status | Resolved |
|
||||
|
||||
**Description:** `AddressSpaceComposition` carries the UNS topology (`UnsAreas` + `UnsLines`), and
|
||||
`AddressSpaceApplier.MaterialiseHierarchy` uses each area's/line's `DisplayName` for the OPC UA
|
||||
@@ -63,7 +63,53 @@ in place + `ClearChangeMasks`). Deferred: a complete fix spans the Runtime modul
|
||||
(`OpcUaPublishActor` must honour the new plan flag and call a hierarchy-refresh / rebuild path),
|
||||
which is outside this module's edit boundary.
|
||||
|
||||
**Resolution:** _(Open — deferred: needs a coordinated change in the Runtime module's `OpcUaPublishActor` to act on a UNS-changed plan; an in-`AddressSpacePlan` change alone is inert because `Apply`/`MaterialiseHierarchy` do not refresh existing folder names.)_
|
||||
**Resolution:** Resolved — 2026-06-20 (SHA pending): a UNS Area / Line **rename-only** deploy now produces a
|
||||
non-empty plan that refreshes the existing folder's `DisplayName` IN PLACE (no rebuild, subscriptions
|
||||
preserved). No EF migration, no entity-schema change, and no wire/proto/Commons-contract break — the
|
||||
`AddressSpacePlan` is a pure in-process runtime diff (never serialized), and `DeploymentArtifact` already
|
||||
round-trips each area/line `Name` into its `UnsAreaProjection`/`UnsLineProjection.DisplayName`, so the
|
||||
artifact mirror needed NO change to carry the new name (byte-parity preserved). Changes span OpcUaServer +
|
||||
Commons + Runtime:
|
||||
|
||||
- **`AddressSpacePlan.cs`** (OpcUaServer) — added an init-only `RenamedFolders` diff set (new nested
|
||||
`FolderRename(FolderNodeId, NewDisplayName)` record) mirroring the EquipmentTag/VirtualTag init-only
|
||||
pattern, and folded it into `IsEmpty`. `AddressSpacePlanner.Compute` now diffs `prev.UnsAreas/UnsLines`
|
||||
vs `next.UnsAreas/UnsLines` by stable id (`UnsAreaId`/`UnsLineId` — the exact NodeId scheme
|
||||
`MaterialiseHierarchy` uses), emitting a rename only when a surviving folder's `DisplayName` differs
|
||||
(ordinal). Added/removed areas are NOT renames (handled by the hierarchy/rebuild path). Output is
|
||||
deterministic (areas first, then lines; each id-sorted).
|
||||
- **`ISurgicalAddressSpaceSink.cs`** (Commons) — added an in-place
|
||||
`bool UpdateFolderDisplayName(folderNodeId, displayName)` to the existing optional surgical-capability
|
||||
interface (already forwarded by `DeferredAddressSpaceSink`); returns false when the folder is missing so
|
||||
the caller rebuilds.
|
||||
- **`OtOpcUaNodeManager.UpdateFolderDisplayName`** (OpcUaServer) — the surgical counterpart of
|
||||
`EnsureFolder` (which early-returns on an existing folder and never touched its name): under `Lock` it
|
||||
mutates the live `FolderState.DisplayName` and calls `ClearChangeMasks` (the SDK gotcha) so subscribers
|
||||
see the new name immediately; NodeId/BrowseName unchanged. `SdkAddressSpaceSink` + `DeferredAddressSpaceSink`
|
||||
forward it (the deferred forward is load-bearing: actors inject the wrapper, so without it the optimization
|
||||
would be inert on every driver-role host — same trap as the F10b surgical-tag forward).
|
||||
- **`AddressSpaceApplier.Apply`** (OpcUaServer) — when no structural rebuild fires, a rename-only (or
|
||||
rename + surgical-tag) plan applies each rename via `UpdateFolderDisplayName`; a sink lacking the surgical
|
||||
capability or a missing folder falls back to a full rebuild (safe default). When a structural rebuild DOES
|
||||
fire (any add/remove/structural change), `MaterialiseHierarchy` re-creates every folder with the new names,
|
||||
so renames are covered for free and no separate surgical call is made. `RenamedFolders.Count` is added to
|
||||
the outcome's `ChangedNodes`.
|
||||
- **`OpcUaPublishActor.HandleRebuild`** (Runtime) — no edit needed beyond the now-non-empty plan: the
|
||||
existing `_applier.Apply(plan)` drives the in-place refresh, and the subsequent idempotent
|
||||
`MaterialiseHierarchy(composition)` leaves the just-renamed folder alone (EnsureFolder early-returns on the
|
||||
existing id). The IsEmpty short-circuit now correctly proceeds for a rename-only deploy.
|
||||
|
||||
**Tests** (all green): `AddressSpacePlannerTests` — area-rename / line-rename / both-renames-ordered yield a
|
||||
non-empty plan with the rename present, and no-change / added-area yield NO rename (no false positives).
|
||||
`AddressSpaceApplierTests` — rename-only updates the folder in place with no rebuild; mixed-with-structural
|
||||
rebuilds; non-surgical sink + surgical-returns-false both fall back to rebuild. `AddressSpaceApplierHierarchyTests`
|
||||
— a real-SDK end-to-end apply asserts the existing area+line `FolderState.DisplayName` is swapped in place
|
||||
with no node-count change. `DeferredAddressSpaceSinkTests` (OpcUaServer + Commons) — the deferred wrapper
|
||||
forwards the new capability and returns the inner's result / false when not surgical.
|
||||
`OpcUaPublishActorRebuildTests.Rebuild_with_area_rename_only_updates_folder_in_place_without_rebuild` — drives
|
||||
the actor end-to-end: a second deploy changing ONLY the area Name reaches the apply path and issues a surgical
|
||||
`UpdateFolderDisplayName("area-1", "Plant South")` without a second full rebuild. Suites:
|
||||
`OpcUaServer.Tests` 297/297, `Runtime.Tests` 278/278, `Commons.Tests` deferred-sink 11/11.
|
||||
|
||||
### OpcUaServer-002
|
||||
|
||||
|
||||
Reference in New Issue
Block a user