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:
Joseph Doherty
2026-06-20 23:10:24 -04:00
parent 94eec70fb0
commit 23b42b424d
13 changed files with 700 additions and 11 deletions
+49 -3
View File
@@ -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