fix(client-ui): resolve Low code-review findings (Client.UI-003,004,006,009,010,011)
- Client.UI-003: wire Serilog properly per CLAUDE.md — console sink + rolling daily file sink in Program.Main, Log.CloseAndFlush in finally, per-VM Log.ForContext<> loggers. - Client.UI-004: migrate the cert-store folder picker from the obsolete OpenFolderDialog to StorageProvider.OpenFolderPickerAsync (with TryGetFolderFromPathAsync seed + TryGetLocalPath extraction). - Client.UI-006: surface formerly silent catch blocks via an observable StatusMessage on the Subscriptions / Alarms VMs that bubbles up into the shell's status bar; soft fallbacks log at Information level so hard failures stay distinguishable. - Client.UI-009: docs/Client.UI.md now lists Standard Deviation in the Aggregate row of the Query Options table. - Client.UI-010: removed the unused MinDateTimeProperty / MaxDateTimeProperty styled properties from DateTimeRangePicker. - Client.UI-011: updated the cert-store TextBox watermark from the legacy AppData/LmxOpcUaClient/pki to the canonical AppData/OtOpcUaClient/pki. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -138,4 +138,21 @@ public class AlarmsViewModelTests
|
||||
{
|
||||
_vm.Interval.ShouldBe(1000);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Regression test for Client.UI-006 — when SubscribeAlarmsAsync throws, the failure must be
|
||||
/// surfaced to the operator via the view model's StatusMessage rather than silently swallowed.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Subscribe_OnFailure_SurfacesStatusMessage()
|
||||
{
|
||||
_vm.IsConnected = true;
|
||||
_service.SubscribeAlarmsException = new Exception("Server returned BadSubscriptionIdInvalid");
|
||||
|
||||
await _vm.SubscribeCommand.ExecuteAsync(null);
|
||||
|
||||
_vm.IsSubscribed.ShouldBeFalse();
|
||||
_vm.StatusMessage.ShouldNotBeNullOrWhiteSpace();
|
||||
_vm.StatusMessage.ShouldContain("BadSubscriptionIdInvalid");
|
||||
}
|
||||
}
|
||||
@@ -175,12 +175,23 @@ public sealed class FakeOpcUaClientService : IOpcUaClientService
|
||||
return Task.FromResult(BrowseResults);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the exception thrown to simulate subscribe failures in the UI.
|
||||
/// </summary>
|
||||
public Exception? SubscribeException { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the exception thrown to simulate alarm-subscribe failures in the UI.
|
||||
/// </summary>
|
||||
public Exception? SubscribeAlarmsException { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task SubscribeAsync(NodeId nodeId, int intervalMs = 1000, CancellationToken ct = default)
|
||||
{
|
||||
SubscribeCallCount++;
|
||||
LastSubscribeNodeId = nodeId;
|
||||
LastSubscribeIntervalMs = intervalMs;
|
||||
if (SubscribeException != null) throw SubscribeException;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
@@ -196,6 +207,7 @@ public sealed class FakeOpcUaClientService : IOpcUaClientService
|
||||
public Task SubscribeAlarmsAsync(NodeId? sourceNodeId = null, int intervalMs = 1000, CancellationToken ct = default)
|
||||
{
|
||||
SubscribeAlarmsCallCount++;
|
||||
if (SubscribeAlarmsException != null) throw SubscribeAlarmsException;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
|
||||
@@ -515,6 +515,25 @@ public class MainWindowViewModelTests
|
||||
_settingsService.LastSaved!.SubscribedNodes.ShouldContain("ns=2;s=TestSub");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Regression test for Client.UI-006 — when GetRedundancyInfoAsync throws (the server
|
||||
/// does not implement the redundancy facet) the connection must still succeed and the
|
||||
/// view model must leave RedundancyInfo null without crashing or hiding the diagnostic.
|
||||
/// The Status text is expected to remain "Connected" (redundancy is optional).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ConnectCommand_RedundancyFailure_DoesNotBreakConnection()
|
||||
{
|
||||
_service.RedundancyException = new Exception("BadServiceUnsupported");
|
||||
|
||||
await _vm.ConnectCommand.ExecuteAsync(null);
|
||||
|
||||
_vm.ConnectionState.ShouldBe(ConnectionState.Connected);
|
||||
_vm.RedundancyInfo.ShouldBeNull();
|
||||
// Connection succeeded; status reflects connection, not the optional redundancy probe failure
|
||||
_vm.StatusMessage.ShouldContain("Connected");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that saved subscriptions are restored after reconnecting the shell.
|
||||
/// </summary>
|
||||
|
||||
@@ -276,6 +276,41 @@ public class SubscriptionsViewModelTests
|
||||
_service.SubscribeCallCount.ShouldBe(2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Regression test for Client.UI-006 — when SubscribeAsync throws, the failure must be surfaced
|
||||
/// to the operator via the view model's StatusMessage rather than silently swallowed.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task AddSubscription_OnFailure_SurfacesStatusMessage()
|
||||
{
|
||||
_vm.IsConnected = true;
|
||||
_vm.NewNodeIdText = "ns=2;s=SomeNode";
|
||||
_service.SubscribeException = new Exception("Permission denied");
|
||||
|
||||
await _vm.AddSubscriptionCommand.ExecuteAsync(null);
|
||||
|
||||
_vm.ActiveSubscriptions.ShouldBeEmpty();
|
||||
_vm.StatusMessage.ShouldNotBeNullOrWhiteSpace();
|
||||
_vm.StatusMessage.ShouldContain("Permission denied");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Regression test for Client.UI-006 — silent swallow when adding a subscription for a node
|
||||
/// (the context-menu helper) must also surface a status to the operator.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task AddSubscriptionForNodeAsync_OnFailure_SurfacesStatusMessage()
|
||||
{
|
||||
_vm.IsConnected = true;
|
||||
_service.SubscribeException = new Exception("Bad node id");
|
||||
|
||||
await _vm.AddSubscriptionForNodeAsync("ns=2;s=ContextMenuNode");
|
||||
|
||||
_vm.ActiveSubscriptions.ShouldBeEmpty();
|
||||
_vm.StatusMessage.ShouldNotBeNullOrWhiteSpace();
|
||||
_vm.StatusMessage.ShouldContain("Bad node id");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddSubscriptionRecursiveAsync_RecursesNestedObjects()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user