From b716fae905c7d59b9140e923a7dd101c14eee7cd Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 15 Aug 2026 14:56:51 +0700 Subject: [PATCH 1/5] fix: restore static DataSet identity after wizard initialization --- ...lSelectionWizardWindow.DataSetAuthority.cs | 30 +++++++++++++------ 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/SignalSelectionWizardWindow.DataSetAuthority.cs b/SignalSelectionWizardWindow.DataSetAuthority.cs index 0bce68562..27c93dfc8 100644 --- a/SignalSelectionWizardWindow.DataSetAuthority.cs +++ b/SignalSelectionWizardWindow.DataSetAuthority.cs @@ -7,27 +7,41 @@ namespace ArIED61850Tester; public partial class SignalSelectionWizardWindow { protected override void OnInitialized(EventArgs e) + { + RestoreAuthoritativeDataSetInventory(); + base.OnInitialized(e); + } + + protected override void OnContentRendered(EventArgs e) + { + // The legacy selector constructor prepares display text before WPF finishes + // initialization. Run authority restoration again after construction so no + // runtime primary leaf can survive as a replacement for a static FCDA/FCD + // identity. The merge is idempotent and does not alter user selection. + RestoreAuthoritativeDataSetInventory(); + SignalsView.Refresh(); + RefreshViewState(); + base.OnContentRendered(e); + } + + private void RestoreAuthoritativeDataSetInventory() { var merge = Iec61850DataSetSignalInventoryService.EnsureMandatorySignals(_device); foreach (var signal in merge.AddedSignals) { // DisplayReference is the engine-authoritative static FCDA/FCD identity. - // Do not rewrite it from ObjectReference: ObjectReference may point to the - // resolved runtime leaf (for example .stVal) while Signal Selection must - // continue to show the exact DataSet member. + // ObjectReference may point to a resolved runtime leaf such as .stVal. if (string.IsNullOrWhiteSpace(signal.DisplayReference)) { signal.DisplayReference = Iec61850MonitorPoint.StripIedNamePrefix( signal.ObjectReference, _device.Name); } + + signal.PropertyChanged -= Signal_PropertyChanged; signal.PropertyChanged += Signal_PropertyChanged; } - // Window initialization can run inside InitializeComponent(), before the caller's - // object initializer assigns Owner. Register recovered rows through the actual - // application MainWindow so they receive the same owner/property-change lifecycle - // as rows produced by the normal discovery pipeline. if (Application.Current?.MainWindow is MainWindow mainWindow) mainWindow.RegisterRecoveredDataSetSignals(_device, merge); else @@ -35,7 +49,5 @@ protected override void OnInitialized(EventArgs e) _device.RecountSelectedSignals(); _device.RefreshComputed(); } - - base.OnInitialized(e); } } From 62209f839dafd3018b6b6bf90f34819d8bff5dfa Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 15 Aug 2026 14:59:04 +0700 Subject: [PATCH 2/5] chore: pin member-centric ARIEC DataSet inventory --- engines/ARIEC61850.lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/engines/ARIEC61850.lock.json b/engines/ARIEC61850.lock.json index fb2c9b864..cb3212075 100644 --- a/engines/ARIEC61850.lock.json +++ b/engines/ARIEC61850.lock.json @@ -2,7 +2,7 @@ "schemaVersion": 1, "repository": "masarray/ARIEC61850", "ref": "main", - "commit": "4fc3119be2bb97e0ea3a8d31fb990e6a1a089e60", - "sourcePullRequest": 77, - "purpose": "Pins the ARIEC61850 engine used by ARSAS. PR #76 preserves every mandatory static DataSet member, including unresolved object-level FCD members without inventing .stVal. PR #77 canonicalizes SCL DataSet member identity across logical devices so parser display references such as IED/LD/LN.DO [FC] resolve to the same IEDLD/LN.DO identity used by the live model." + "commit": "761fa0df9ca84fbe15352d2670f03fddbe8784ba", + "sourcePullRequest": 78, + "purpose": "Pins the ARIEC61850 engine used by ARSAS. PR #76 preserves unresolved static DataSet members, PR #77 canonicalizes cross-logical-device SCL references, and PR #78 makes mandatory Signal Selection inventory protocol-member-centric: exactly one descriptor per static DataSet member while keeping resolved runtime primary leaves separate from the original FCDA/FCD identity." } From 3e10e1ef37a736eb36477e0f79fc646609cb0821 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 15 Aug 2026 14:59:56 +0700 Subject: [PATCH 3/5] fix: remove launcher tint before Loaded-priority card discovery --- MainWindow.FirstRunLauncherRepair.cs | 36 ++++++++++++++++++---------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/MainWindow.FirstRunLauncherRepair.cs b/MainWindow.FirstRunLauncherRepair.cs index 88c3c34c4..f29a6a2fe 100644 --- a/MainWindow.FirstRunLauncherRepair.cs +++ b/MainWindow.FirstRunLauncherRepair.cs @@ -28,28 +28,38 @@ private static void FirstRunLauncherRepair_Loaded(object sender, RoutedEventArgs if (sender is not MainWindow window) return; - // MainWindow.IoTesting queues its normal launcher at Loaded priority. Run after - // that attempt so this path is only a compatibility repair, never a replacement. + // MainWindow.IoTesting queued InstallFirstRunTestingChoices at Loaded priority + // during OnInitialized. Remove the decorative Border synchronously while handling + // Loaded, before that queued callback can execute SingleOrDefault over hero Borders. + // This prevents the startup exception instead of merely repairing the launcher + // after the exception was swallowed by the dispatcher error handler. + window.RemoveFirstRunHeroTintBeforeLauncherDiscovery(); + + // Run the compatibility audit after the normal launcher attempt. At this point it + // only restores actions/Z-order or retries if some unrelated condition prevented + // the normal launcher from materializing. window.Dispatcher.BeginInvoke( new Action(window.RestoreFirstRunLauncherContract), DispatcherPriority.ContextIdle); } + private void RemoveFirstRunHeroTintBeforeLauncherDiscovery() + { + var heroGrid = FindFirstRunHeroGrid(); + var tint = heroGrid?.Children + .OfType() + .FirstOrDefault(border => Equals(border.Tag, "P2IndustrialHeroTint")); + if (heroGrid != null && tint != null) + heroGrid.Children.Remove(tint); + } + private void RestoreFirstRunLauncherContract() { if (_ioListTestingLauncherCard == null) { - var heroGrid = FindFirstRunHeroGrid(); - var tint = heroGrid?.Children - .OfType() - .FirstOrDefault(border => Equals(border.Tag, "P2IndustrialHeroTint")); - - // P2.1 originally inserted its tint as a Border. The legacy launcher - // intentionally looked for the single Border that represented the general - // testing card, so the decorative Border made SingleOrDefault fail. - if (heroGrid != null && tint != null) - heroGrid.Children.Remove(tint); - + // Defensive retry: remove a tint that may have been re-applied between Loaded + // and ContextIdle, then invoke the legacy launcher against one operational card. + RemoveFirstRunHeroTintBeforeLauncherDiscovery(); InstallFirstRunTestingChoices(); if (_ioListTestingLauncherCard == null) From d607ffbf29ce38435562a707320a9c0e225da126 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 15 Aug 2026 15:00:15 +0700 Subject: [PATCH 4/5] test: guard post-construction DataSet authority restoration --- ...neDataSetSignalSelectionRegressionTests.cs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/ARSAS.Tests/OfflineDataSetSignalSelectionRegressionTests.cs b/tests/ARSAS.Tests/OfflineDataSetSignalSelectionRegressionTests.cs index 202567b7e..fe6a15ba1 100644 --- a/tests/ARSAS.Tests/OfflineDataSetSignalSelectionRegressionTests.cs +++ b/tests/ARSAS.Tests/OfflineDataSetSignalSelectionRegressionTests.cs @@ -34,6 +34,30 @@ public void SignalSelectionRecovery_DoesNotOverwriteStaticFcdaDisplayIdentity() StringComparison.Ordinal); } + [Fact] + public void SignalSelectionRecovery_RunsAgainAfterLegacyConstructorDisplayPreparation() + { + var source = File.ReadAllText(FindRepoFile("SignalSelectionWizardWindow.DataSetAuthority.cs")); + + Assert.Contains("protected override void OnContentRendered(EventArgs e)", source, StringComparison.Ordinal); + Assert.Contains("RestoreAuthoritativeDataSetInventory();", source, StringComparison.Ordinal); + Assert.Contains("SignalsView.Refresh();", source, StringComparison.Ordinal); + Assert.Contains( + "runtime primary leaf can survive as a replacement for a static FCDA/FCD", + source, + StringComparison.Ordinal); + } + + [Fact] + public void EngineLock_PinsMemberCentricStaticDataSetInventory() + { + var source = File.ReadAllText(FindRepoFile("engines/ARIEC61850.lock.json")); + + Assert.Contains("761fa0df9ca84fbe15352d2670f03fddbe8784ba", source, StringComparison.OrdinalIgnoreCase); + Assert.Contains("\"sourcePullRequest\": 78", source, StringComparison.Ordinal); + Assert.Contains("one descriptor per static DataSet member", source, StringComparison.OrdinalIgnoreCase); + } + [Fact] public void CompletenessReport_SamplesEveryFailingDataSetAndSeparatesSemanticDescriptors() { From 3e0d441f58f29b5a53d7c0c9379c400f9ee09246 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 15 Aug 2026 15:00:33 +0700 Subject: [PATCH 5/5] test: guard first-run tint removal before launcher discovery --- .../FirstRunLauncherRegressionTests.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/ARSAS.Tests/FirstRunLauncherRegressionTests.cs b/tests/ARSAS.Tests/FirstRunLauncherRegressionTests.cs index 6219c136a..5e16ace45 100644 --- a/tests/ARSAS.Tests/FirstRunLauncherRegressionTests.cs +++ b/tests/ARSAS.Tests/FirstRunLauncherRegressionTests.cs @@ -2,6 +2,21 @@ namespace ARSAS.Tests; public sealed class FirstRunLauncherRegressionTests { + [Fact] + public void FirstRunLauncher_RemovesDecorativeBorderSynchronouslyBeforeQueuedDiscovery() + { + var source = File.ReadAllText(FindRepoFile("MainWindow.FirstRunLauncherRepair.cs")); + + var loadedHandler = source.IndexOf("private static void FirstRunLauncherRepair_Loaded", StringComparison.Ordinal); + var removeBeforeQueue = source.IndexOf("window.RemoveFirstRunHeroTintBeforeLauncherDiscovery();", loadedHandler, StringComparison.Ordinal); + var queueContextIdle = source.IndexOf("window.Dispatcher.BeginInvoke", loadedHandler, StringComparison.Ordinal); + + Assert.True(loadedHandler >= 0); + Assert.True(removeBeforeQueue > loadedHandler); + Assert.True(queueContextIdle > removeBeforeQueue); + Assert.Contains("before that queued callback can execute SingleOrDefault", source, StringComparison.Ordinal); + } + [Fact] public void FirstRunLauncher_RepairsP2OverlayBeforeRebuildingOperationalCards() {