Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions IedCardActionsGearPolicy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ namespace ArIED61850Tester;
/// <summary>
/// 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).
/// </summary>
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
Expand Down
2 changes: 1 addition & 1 deletion IedWorkspaceActionsCardBehavior.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
9 changes: 8 additions & 1 deletion MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -766,7 +766,14 @@ private async Task<bool> 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);
Comment on lines 768 to +776

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Serialize the post-discovery chooser

When two per-device discoveries are already running concurrently, the first completion enters this modal chooser without setting _signalSelectionWizardOpen; the second completion can therefore pass the same guard inside the first dialog's nested dispatcher loop and open another ShowDialog. This stacks the second IED's chooser over the first and changes SelectedDevice behind it, whereas the previous signal-selection path set the global guard before showing its window. Track the actions chooser with the same single-workflow guard, and cover concurrent discovery completions in a UI interaction regression test.

AGENTS.md reference: AGENTS.md:L180-L180

Useful? React with 👍 / 👎.

else
SetStatus($"{device.Name}: discovery complete. Use the edit icon on its IED card to review {restoredCount} restored selection(s).");
}
Expand Down
6 changes: 3 additions & 3 deletions SclSignalSelectionModeWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,14 @@
<Button Grid.Column="0" Style="{StaticResource PrimaryButton}" Padding="14,11"
Click="MonitorStaticDataSet_Click" ToolTip="Select authoritative static DataSet members and start online monitoring">
<StackPanel>
<TextBlock Text="Static DataSet" FontWeight="SemiBold" HorizontalAlignment="Center"/>
<TextBlock Text="Select + monitor" FontSize="10.5" Opacity="0.82" HorizontalAlignment="Center"/>
<TextBlock Text="DataSet Signals" FontWeight="SemiBold" HorizontalAlignment="Center"/>
<TextBlock Text="Static report-only" FontSize="10.5" Opacity="0.82" HorizontalAlignment="Center"/>
</StackPanel>
</Button>
<Button Grid.Column="2" Style="{StaticResource SoftButton}" Padding="14,11"
Click="MonitorManual_Click" ToolTip="Choose exact ST, MX and control points, then start monitoring">
<StackPanel>
<TextBlock Text="Select Signals" FontWeight="SemiBold" HorizontalAlignment="Center"/>
<TextBlock Text="Signal Catalog" FontWeight="SemiBold" HorizontalAlignment="Center"/>
<TextBlock Text="Manual + monitor" FontSize="10.5" Foreground="{StaticResource Muted}" HorizontalAlignment="Center"/>
</StackPanel>
</Button>
Expand Down
2 changes: 1 addition & 1 deletion docs/static-dataset-report-only-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion tests/ARSAS.Tests/IedCardActionsGearRegressionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
92 changes: 92 additions & 0 deletions tests/ARSAS.Tests/LiveDiscoveryPostModelWorkflowRegressionTests.cs
Original file line number Diff line number Diff line change
@@ -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<bool> ConnectAndConfigureDeviceAsync",
StringComparison.Ordinal);
var end = source.IndexOf(
"private async Task<bool> 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<string>()", 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);
}
}
6 changes: 3 additions & 3 deletions tests/ARSAS.Tests/SclTaskFirstQuickStartRegressionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
4 changes: 2 additions & 2 deletions tests/ARSAS.Tests/WorkspaceModeSwitchTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading