From 2c19a50bf951073468f793eb884c40fa4a9c711a Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Wed, 5 Aug 2026 03:38:25 -0400 Subject: [PATCH] fix(raw): a superseded manual-entry open can no longer feed the pickers another device's config --- .../Shared/Raw/RawManualTagEntryModal.razor | 8 +++++ ...RawManualTagEntryModalMergedConfigTests.cs | 36 +++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Raw/RawManualTagEntryModal.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Raw/RawManualTagEntryModal.razor index aa96d398..1042c96e 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Raw/RawManualTagEntryModal.razor +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Raw/RawManualTagEntryModal.razor @@ -160,6 +160,9 @@ private bool _summaryOk; private bool _open; // guards re-seeding rows on unrelated re-renders + // Identifies WHICH open a post-await write belongs to; _open alone cannot tell two opens apart. + private int _openGeneration; + // Merged Driver+Device config for each row's address picker — the same blob Test-connect and // RawBrowseModal dial, resolved once per open; null (→ "{}") when the resolve failed: the picker's // browse degrades to its graceful error chip. @@ -175,6 +178,7 @@ } if (_open) { return; } // same open, re-render → preserve in-progress rows _open = true; + var generation = ++_openGeneration; _rows.Clear(); _rows.Add(new RowModel()); _summary = null; @@ -186,6 +190,10 @@ try { var merged = await Svc.LoadMergedProbeConfigAsync(DeviceId); + // A close-reopen onto another device can complete while this resolve is in flight, so a + // superseded open must never install its device's config under the displayed grid. + if (generation != _openGeneration || !Visible) { return; } + if (merged is { } m) { _mergedConfigJson = m.MergedConfigJson; } } catch diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Raw/RawManualTagEntryModalMergedConfigTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Raw/RawManualTagEntryModalMergedConfigTests.cs index b81c38d8..042e313e 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Raw/RawManualTagEntryModalMergedConfigTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Raw/RawManualTagEntryModalMergedConfigTests.cs @@ -94,6 +94,42 @@ public sealed class RawManualTagEntryModalMergedConfigTests accessor().ShouldBe(merged); } + [Fact] + public async Task A_stale_open_never_feeds_the_picker_another_devices_config() + { + const string deviceA = """{"endpoint":"10.0.0.1:44818","slot":0}"""; + const string deviceB = """{"endpoint":"10.0.0.2:44818","slot":1}"""; + + var deviceAResolve = new TaskCompletionSource(); + var svc = new FakeRawTreeService + { + OnLoadMergedProbeConfigAsync = async deviceId => + { + if (deviceId == "dev-A") { await deviceAResolve.Task; return ("AbCip", deviceA); } + return ("AbCip", deviceB); + }, + }; + + var modal = new RawManualTagEntryModal(); + InjectService(modal, svc); + + // Quick close-reopen onto a different device while the first resolve is still in flight — the + // grid shows dev-B, so dev-A's late continuation must not install its config underneath it. + SetParameters(modal, visible: true, deviceId: "dev-A", driverType: "AbCip"); + var staleOpen = OpenAsync(modal); + + SetParameters(modal, visible: false, deviceId: "dev-A", driverType: "AbCip"); + await OpenAsync(modal); + + SetParameters(modal, visible: true, deviceId: "dev-B", driverType: "AbCip"); + await OpenAsync(modal); + + deviceAResolve.SetResult(); + await staleOpen; + + GetDriverConfigJson(modal).ShouldBe(deviceB); + } + /// /// Sets the modal's [Parameter]s. By reflection because BL0005 forbids assigning component /// parameters from outside the component, and this project treats warnings as errors.