diff --git a/IedCardActionsGearPolicy.cs b/IedCardActionsGearPolicy.cs index 0fbed72d3..e15ea5ec9 100644 --- a/IedCardActionsGearPolicy.cs +++ b/IedCardActionsGearPolicy.cs @@ -11,13 +11,13 @@ namespace ArIED61850Tester; /// /// Adds a dedicated Lucide-style gear button to every IED card. The existing pencil /// remains the direct Edit Signals shortcut; the gear is the explicit entry point for -/// the reusable IED Actions chooser (Static DataSet, Select Signals, RCB Engineering, +/// the reusable IED Actions chooser (DataSet Signals, Signal Catalog, RCB Engineering, /// COMTRADE and Browse Offline). /// internal static class IedCardActionsGearPolicy { private const string GearUid = "ARSAS.IedActionsGear"; - private const string GearToolTip = "IED Actions — Static DataSet, Select Signals, RCB Engineering, COMTRADE, Browse Offline"; + private const string GearToolTip = "IED Actions — DataSet Signals, Signal Catalog, RCB Engineering, COMTRADE, Browse Offline"; private const double GearOpticalSize = 16d; // Exact Lucide Settings outline supplied by the product owner, translated from the diff --git a/IedWorkspaceActionsCardBehavior.cs b/IedWorkspaceActionsCardBehavior.cs index fb557b7f0..a93b8950d 100644 --- a/IedWorkspaceActionsCardBehavior.cs +++ b/IedWorkspaceActionsCardBehavior.cs @@ -14,7 +14,7 @@ namespace ArIED61850Tester; internal static class IedWorkspaceActionsCardBehavior { private const string LegacyToolTip = "Configure Signals"; - private const string ActionsToolTip = "IED Actions — Static DataSet, Select Signals, RCB Engineering, COMTRADE, Browse Offline"; + private const string ActionsToolTip = "IED Actions — DataSet Signals, Signal Catalog, RCB Engineering, COMTRADE, Browse Offline"; [ModuleInitializer] internal static void Initialize() diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs index 69eae95b6..2a00702ee 100644 --- a/MainWindow.xaml.cs +++ b/MainWindow.xaml.cs @@ -766,7 +766,14 @@ private async Task ConnectAndConfigureDeviceAsync( if (openWizard && device.SignalCount > 0) { if ((selectDevice || ReferenceEquals(SelectedDevice, device)) && !_signalSelectionWizardOpen) - await OpenSignalSelectionWizardAsync(device, restoredCount); + // Live discovery and Open SCL are only different model-ingestion + // sources. Once the typed IED model exists, both must enter the same + // operator decision: exact DataSet Signals in deterministic static + // report-only mode, or the broader manual Signal Catalog in Hybrid + // mode. Opening the manual wizard directly here silently skipped the + // Static DataSet authority and left newly discovered IEDs on mixed + // static/dynamic reporting plus MMS polling fallback. + await OpenIedWorkspaceActionsAsync(device); else SetStatus($"{device.Name}: discovery complete. Use the edit icon on its IED card to review {restoredCount} restored selection(s)."); } diff --git a/SclSignalSelectionModeWindow.xaml b/SclSignalSelectionModeWindow.xaml index f4668d997..573a3c11d 100644 --- a/SclSignalSelectionModeWindow.xaml +++ b/SclSignalSelectionModeWindow.xaml @@ -52,14 +52,14 @@ diff --git a/docs/static-dataset-report-only-contract.md b/docs/static-dataset-report-only-contract.md index b9df6ab8e..8489e168a 100644 --- a/docs/static-dataset-report-only-contract.md +++ b/docs/static-dataset-report-only-contract.md @@ -11,7 +11,7 @@ This document is the acceptance boundary for ARSAS Static DataSet monitoring. - Dynamic DataSet writes are disabled in this mode. - ARIEC planner fallback candidates are diagnostic evidence only and must never become silent MMS polling. -Manual / Select Signals mode keeps the normal Hybrid acquisition behavior and is intentionally outside this contract. +Manual / Signal Catalog mode keeps the normal Hybrid acquisition behavior and is intentionally outside this contract. ## Deterministic configured-RCB path diff --git a/tests/ARSAS.Tests/IedCardActionsGearRegressionTests.cs b/tests/ARSAS.Tests/IedCardActionsGearRegressionTests.cs index f7d7ff410..de542687f 100644 --- a/tests/ARSAS.Tests/IedCardActionsGearRegressionTests.cs +++ b/tests/ARSAS.Tests/IedCardActionsGearRegressionTests.cs @@ -8,7 +8,7 @@ public void IedCard_HasDedicatedGearEntryPointForReusableActions() var source = File.ReadAllText(FindRepoFile("IedCardActionsGearPolicy.cs")); Assert.Contains("GearUid = \"ARSAS.IedActionsGear\"", source, StringComparison.Ordinal); - Assert.Contains("IED Actions — Static DataSet, Select Signals, RCB Engineering, COMTRADE, Browse Offline", source, StringComparison.Ordinal); + Assert.Contains("IED Actions — DataSet Signals, Signal Catalog, RCB Engineering, COMTRADE, Browse Offline", source, StringComparison.Ordinal); Assert.Contains("OpenIedWorkspaceActionsAsync(device)", source, StringComparison.Ordinal); Assert.Contains("actionBar.Children.Add(CreateGearButton(device))", source, StringComparison.Ordinal); } diff --git a/tests/ARSAS.Tests/LiveDiscoveryPostModelWorkflowRegressionTests.cs b/tests/ARSAS.Tests/LiveDiscoveryPostModelWorkflowRegressionTests.cs new file mode 100644 index 000000000..c7021a2f5 --- /dev/null +++ b/tests/ARSAS.Tests/LiveDiscoveryPostModelWorkflowRegressionTests.cs @@ -0,0 +1,92 @@ +namespace ARSAS.Tests; + +public sealed class LiveDiscoveryPostModelWorkflowRegressionTests +{ + [Fact] + public void SuccessfulDiscovery_UsesSharedPostModelChooser_InsteadOfOpeningManualCatalogDirectly() + { + var source = Read("MainWindow.xaml.cs"); + var start = source.IndexOf( + "private async Task ConnectAndConfigureDeviceAsync", + StringComparison.Ordinal); + var end = source.IndexOf( + "private async Task ConnectUsingSavedModelAsync", + start, + StringComparison.Ordinal); + Assert.True(start >= 0 && end > start); + var discovery = source[start..end]; + + Assert.Contains("device.LiveDiscoveryModel", Read("Services/Iec61850MonitorRuntime.cs"), StringComparison.Ordinal); + Assert.Contains("await OpenIedWorkspaceActionsAsync(device);", discovery, StringComparison.Ordinal); + Assert.DoesNotContain("OpenSignalSelectionWizardAsync(device, restoredCount)", discovery, StringComparison.Ordinal); + } + + [Fact] + public void SharedPostModelChooser_RoutesDataSetSignalsToExactStaticReportOnlyAuthority() + { + var actions = Read("MainWindow.SclQuickActions.cs"); + var sharedAuthority = Read("MainWindow.SharedSclWorkspace.cs"); + var staticRuntime = Read("Services/NativeIec61850Client.StaticDataSetReporting.cs"); + + var staticStart = actions.IndexOf("if (dialog.UseStaticDataSet)", StringComparison.Ordinal); + var manualStart = actions.IndexOf("var accepted = await OpenSignalSelectionWizardAsync", staticStart, StringComparison.Ordinal); + Assert.True(staticStart >= 0 && manualStart > staticStart); + var staticChoice = actions[staticStart..manualStart]; + + Assert.Contains("ApplyStaticDataSetSelection(device);", staticChoice, StringComparison.Ordinal); + Assert.Contains("await StartDeviceMonitorAsync(device)", staticChoice, StringComparison.Ordinal); + Assert.DoesNotContain("UseHybrid", staticChoice, StringComparison.Ordinal); + + Assert.Contains("Iec61850DataSetSignalInventoryService.EnsureMandatorySignals(device)", sharedAuthority, StringComparison.Ordinal); + Assert.Contains("Iec61850StaticDataSetAuthoritySelection.Build(device)", sharedAuthority, StringComparison.Ordinal); + Assert.Contains("Iec61850MonitoringModeRegistry.UseStaticDataSetReportOnly(device)", sharedAuthority, StringComparison.Ordinal); + + Assert.Contains("device.SclWorkspace?.DesignModel ?? device.LiveDiscoveryModel", staticRuntime, StringComparison.Ordinal); + Assert.Contains("AllowDynamicDataSetWrites = false", staticRuntime, StringComparison.Ordinal); + Assert.Contains("PollingPointKeys = Array.Empty()", staticRuntime, StringComparison.Ordinal); + Assert.Contains("PollingFallbackSignalCount = 0", staticRuntime, StringComparison.Ordinal); + } + + [Fact] + public void SharedPostModelChooser_RoutesSignalCatalogToHybridOnlyAfterManualSelectionIsAccepted() + { + var actions = Read("MainWindow.SclQuickActions.cs"); + var manualStart = actions.IndexOf("var accepted = await OpenSignalSelectionWizardAsync", StringComparison.Ordinal); + var methodEnd = actions.IndexOf("internal async Task OpenIedWorkspaceActionsFromCardAsync", manualStart, StringComparison.Ordinal); + Assert.True(manualStart >= 0 && methodEnd > manualStart); + var manualChoice = actions[manualStart..methodEnd]; + + var acceptedGuard = manualChoice.IndexOf("if (!accepted)", StringComparison.Ordinal); + var hybridSwitch = manualChoice.IndexOf("Iec61850MonitoringModeRegistry.UseHybrid(device)", StringComparison.Ordinal); + Assert.True(acceptedGuard >= 0 && hybridSwitch > acceptedGuard); + Assert.Contains("autoStartAfterSave: false", manualChoice, StringComparison.Ordinal); + Assert.Contains("await StartDeviceMonitorAsync(device)", manualChoice, StringComparison.Ordinal); + } + + [Fact] + public void Chooser_UsesSourceNeutralSignalSourceLabels() + { + var xaml = Read("SclSignalSelectionModeWindow.xaml"); + + Assert.Contains("DataSet Signals", xaml, StringComparison.Ordinal); + Assert.Contains("Static report-only", xaml, StringComparison.Ordinal); + Assert.Contains("Signal Catalog", xaml, StringComparison.Ordinal); + } + + private static string Read(string relativePath) + => File.ReadAllText(FindRepoFile(relativePath)).Replace("\r\n", "\n", StringComparison.Ordinal); + + private static string FindRepoFile(string relativePath) + { + DirectoryInfo? directory = new(AppContext.BaseDirectory); + while (directory != null) + { + var candidate = Path.Combine(directory.FullName, relativePath); + if (File.Exists(candidate)) + return candidate; + directory = directory.Parent; + } + + throw new FileNotFoundException(relativePath); + } +} diff --git a/tests/ARSAS.Tests/SclTaskFirstQuickStartRegressionTests.cs b/tests/ARSAS.Tests/SclTaskFirstQuickStartRegressionTests.cs index 83156482e..713008514 100644 --- a/tests/ARSAS.Tests/SclTaskFirstQuickStartRegressionTests.cs +++ b/tests/ARSAS.Tests/SclTaskFirstQuickStartRegressionTests.cs @@ -10,8 +10,8 @@ public void QuickStart_SeparatesOfflineEngineeringFromMonitoringIntent() Assert.Contains("ContextHeading", xaml, StringComparison.Ordinal); Assert.Contains("Workspace opened offline", code, StringComparison.Ordinal); - Assert.Contains("Static DataSet", xaml, StringComparison.Ordinal); - Assert.Contains("Select Signals", xaml, StringComparison.Ordinal); + Assert.Contains("DataSet Signals", xaml, StringComparison.Ordinal); + Assert.Contains("Signal Catalog", xaml, StringComparison.Ordinal); Assert.Contains("RCB Engineering", xaml, StringComparison.Ordinal); Assert.Contains("Download COMTRADE", xaml, StringComparison.Ordinal); Assert.Contains("Browse Offline", xaml, StringComparison.Ordinal); @@ -61,7 +61,7 @@ public void IedCard_EditAction_ReopensSameChooser_AndPinsActionsToClickedIed() var actions = File.ReadAllText(FindRepoFile("MainWindow.SclQuickActions.cs")); var chooser = File.ReadAllText(FindRepoFile("SclSignalSelectionModeWindow.xaml.cs")); - Assert.Contains("IED Actions — Static DataSet, Select Signals, RCB Engineering, COMTRADE, Browse Offline", behavior, StringComparison.Ordinal); + Assert.Contains("IED Actions — DataSet Signals, Signal Catalog, RCB Engineering, COMTRADE, Browse Offline", behavior, StringComparison.Ordinal); Assert.Contains("args.Handled = true;", behavior, StringComparison.Ordinal); Assert.Contains("OpenIedWorkspaceActionsFromCardAsync(button)", behavior, StringComparison.Ordinal); Assert.Contains("new SclSignalSelectionModeWindow(1, device)", actions, StringComparison.Ordinal); diff --git a/tests/ARSAS.Tests/WorkspaceModeSwitchTests.cs b/tests/ARSAS.Tests/WorkspaceModeSwitchTests.cs index fd76fc1a6..efa9ad9af 100644 --- a/tests/ARSAS.Tests/WorkspaceModeSwitchTests.cs +++ b/tests/ARSAS.Tests/WorkspaceModeSwitchTests.cs @@ -32,8 +32,8 @@ public void SclImport_UsesOneSelectionAuthorityAcrossEngineeringAndFat() Assert.Contains("new SclSignalSelectionModeWindow", workflow, StringComparison.Ordinal); Assert.DoesNotContain("MessageBox.Show", workflow, StringComparison.Ordinal); Assert.Contains("Workspace opened offline", selectionWindow, StringComparison.Ordinal); - Assert.Contains("Static DataSet", selectionWindow, StringComparison.Ordinal); - Assert.Contains("Select Signals", selectionWindow, StringComparison.Ordinal); + Assert.Contains("DataSet Signals", selectionWindow, StringComparison.Ordinal); + Assert.Contains("Signal Catalog", selectionWindow, StringComparison.Ordinal); Assert.Contains("RCB Engineering", selectionWindow, StringComparison.Ordinal); Assert.Contains("Download COMTRADE", selectionWindow, StringComparison.Ordinal); Assert.Contains("Browse Offline", selectionWindow, StringComparison.Ordinal);