12efbffd56
Re-review at 7286d320. -013: AlarmsViewModel.OnAlarmEvent removal path no longer fires a
redundant Replace+Remove (one Remove now), preventing a DataGrid re-paint flash. -012: add
update/remove-path test coverage. + TDD.
524 lines
18 KiB
C#
524 lines
18 KiB
C#
using Opc.Ua;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Client.Shared.Models;
|
|
using ZB.MOM.WW.OtOpcUa.Client.UI.Services;
|
|
using ZB.MOM.WW.OtOpcUa.Client.UI.Tests.Fakes;
|
|
using ZB.MOM.WW.OtOpcUa.Client.UI.ViewModels;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Client.UI.Tests;
|
|
|
|
public class AlarmsViewModelTests
|
|
{
|
|
private readonly FakeOpcUaClientService _service;
|
|
private readonly AlarmsViewModel _vm;
|
|
|
|
/// <summary>Initializes a new test instance.</summary>
|
|
public AlarmsViewModelTests()
|
|
{
|
|
_service = new FakeOpcUaClientService();
|
|
var dispatcher = new SynchronousUiDispatcher();
|
|
_vm = new AlarmsViewModel(_service, dispatcher);
|
|
}
|
|
|
|
/// <summary>Verifies that SubscribeCommand cannot execute when disconnected.</summary>
|
|
[Fact]
|
|
public void SubscribeCommand_CannotExecute_WhenDisconnected()
|
|
{
|
|
_vm.IsConnected = false;
|
|
_vm.SubscribeCommand.CanExecute(null).ShouldBeFalse();
|
|
}
|
|
|
|
/// <summary>Verifies that SubscribeCommand cannot execute when already subscribed.</summary>
|
|
[Fact]
|
|
public void SubscribeCommand_CannotExecute_WhenAlreadySubscribed()
|
|
{
|
|
_vm.IsConnected = true;
|
|
_vm.IsSubscribed = true;
|
|
_vm.SubscribeCommand.CanExecute(null).ShouldBeFalse();
|
|
}
|
|
|
|
/// <summary>Verifies that SubscribeCommand can execute when connected and not subscribed.</summary>
|
|
[Fact]
|
|
public void SubscribeCommand_CanExecute_WhenConnectedAndNotSubscribed()
|
|
{
|
|
_vm.IsConnected = true;
|
|
_vm.IsSubscribed = false;
|
|
_vm.SubscribeCommand.CanExecute(null).ShouldBeTrue();
|
|
}
|
|
|
|
/// <summary>Verifies that SubscribeCommand sets IsSubscribed flag.</summary>
|
|
[Fact]
|
|
public async Task SubscribeCommand_SetsIsSubscribed()
|
|
{
|
|
_vm.IsConnected = true;
|
|
|
|
await _vm.SubscribeCommand.ExecuteAsync(null);
|
|
|
|
_vm.IsSubscribed.ShouldBeTrue();
|
|
_service.SubscribeAlarmsCallCount.ShouldBe(1);
|
|
}
|
|
|
|
/// <summary>Verifies that UnsubscribeCommand cannot execute when not subscribed.</summary>
|
|
[Fact]
|
|
public void UnsubscribeCommand_CannotExecute_WhenNotSubscribed()
|
|
{
|
|
_vm.IsConnected = true;
|
|
_vm.IsSubscribed = false;
|
|
_vm.UnsubscribeCommand.CanExecute(null).ShouldBeFalse();
|
|
}
|
|
|
|
/// <summary>Verifies that UnsubscribeCommand clears IsSubscribed flag.</summary>
|
|
[Fact]
|
|
public async Task UnsubscribeCommand_ClearsIsSubscribed()
|
|
{
|
|
_vm.IsConnected = true;
|
|
await _vm.SubscribeCommand.ExecuteAsync(null);
|
|
|
|
await _vm.UnsubscribeCommand.ExecuteAsync(null);
|
|
|
|
_vm.IsSubscribed.ShouldBeFalse();
|
|
_service.UnsubscribeAlarmsCallCount.ShouldBe(1);
|
|
}
|
|
|
|
/// <summary>Verifies that RefreshCommand calls the service.</summary>
|
|
[Fact]
|
|
public async Task RefreshCommand_CallsService()
|
|
{
|
|
_vm.IsConnected = true;
|
|
_vm.IsSubscribed = true;
|
|
|
|
await _vm.RefreshCommand.ExecuteAsync(null);
|
|
|
|
_service.RequestConditionRefreshCallCount.ShouldBe(1);
|
|
}
|
|
|
|
/// <summary>Verifies that RefreshCommand cannot execute when not subscribed.</summary>
|
|
[Fact]
|
|
public void RefreshCommand_CannotExecute_WhenNotSubscribed()
|
|
{
|
|
_vm.IsConnected = true;
|
|
_vm.IsSubscribed = false;
|
|
_vm.RefreshCommand.CanExecute(null).ShouldBeFalse();
|
|
}
|
|
|
|
/// <summary>Verifies that alarm events are added to the collection.</summary>
|
|
[Fact]
|
|
public void AlarmEvent_AddsToCollection()
|
|
{
|
|
var alarm = new AlarmEventArgs(
|
|
"Source1", "HighAlarm", 500, "Temperature high",
|
|
true, true, false, DateTime.UtcNow);
|
|
|
|
_service.RaiseAlarmEvent(alarm);
|
|
|
|
_vm.AlarmEvents.Count.ShouldBe(1);
|
|
_vm.AlarmEvents[0].SourceName.ShouldBe("Source1");
|
|
_vm.AlarmEvents[0].ConditionName.ShouldBe("HighAlarm");
|
|
_vm.AlarmEvents[0].Severity.ShouldBe((ushort)500);
|
|
_vm.AlarmEvents[0].Message.ShouldBe("Temperature high");
|
|
}
|
|
|
|
/// <summary>Verifies that Clear resets the view model state.</summary>
|
|
[Fact]
|
|
public void Clear_ResetsState()
|
|
{
|
|
_vm.IsSubscribed = true;
|
|
_vm.AlarmEvents.Add(new AlarmEventViewModel("Src", "Cond", 100, "Msg", true, true, false, DateTime.UtcNow));
|
|
|
|
_vm.Clear();
|
|
|
|
_vm.AlarmEvents.ShouldBeEmpty();
|
|
_vm.IsSubscribed.ShouldBeFalse();
|
|
}
|
|
|
|
/// <summary>Verifies that Teardown unregisters the event handler.</summary>
|
|
[Fact]
|
|
public void Teardown_UnhooksEventHandler()
|
|
{
|
|
_vm.Teardown();
|
|
|
|
var alarm = new AlarmEventArgs(
|
|
"Source1", "HighAlarm", 500, "Test",
|
|
true, true, false, DateTime.UtcNow);
|
|
_service.RaiseAlarmEvent(alarm);
|
|
|
|
_vm.AlarmEvents.ShouldBeEmpty();
|
|
}
|
|
|
|
/// <summary>Verifies that the default polling interval is 1000ms.</summary>
|
|
[Fact]
|
|
public void DefaultInterval_Is1000()
|
|
{
|
|
_vm.Interval.ShouldBe(1000);
|
|
}
|
|
|
|
// --- Alarm update and non-retain remove paths (Client.UI-012 / Client.UI-013) ---
|
|
|
|
/// <summary>
|
|
/// Regression test for Client.UI-013 / Client.UI-012 — a second event for the same
|
|
/// source+condition must update the existing row in place rather than adding a duplicate.
|
|
/// </summary>
|
|
[Fact]
|
|
public void AlarmEvent_ExistingAlarm_UpdatesInPlace()
|
|
{
|
|
var first = new AlarmEventArgs(
|
|
"Source1", "HighAlarm", 500, "Temperature high",
|
|
retain: true, activeState: true, ackedState: false, time: DateTime.UtcNow);
|
|
_service.RaiseAlarmEvent(first);
|
|
|
|
var updated = new AlarmEventArgs(
|
|
"Source1", "HighAlarm", 750, "Temperature very high",
|
|
retain: true, activeState: true, ackedState: false, time: DateTime.UtcNow);
|
|
_service.RaiseAlarmEvent(updated);
|
|
|
|
_vm.AlarmEvents.Count.ShouldBe(1);
|
|
_vm.AlarmEvents[0].Severity.ShouldBe((ushort)750);
|
|
_vm.AlarmEvents[0].Message.ShouldBe("Temperature very high");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Regression test for Client.UI-013 — when an existing retained alarm becomes
|
|
/// non-retained the row must be removed from the collection with exactly ONE collection
|
|
/// mutation (not a Replace + Remove pair).
|
|
/// </summary>
|
|
[Fact]
|
|
public void AlarmEvent_ExistingAlarmBecomesNonRetained_IsRemovedCleanly()
|
|
{
|
|
// Seed a retained alarm.
|
|
var first = new AlarmEventArgs(
|
|
"Source1", "HighAlarm", 500, "Active",
|
|
retain: true, activeState: true, ackedState: false, time: DateTime.UtcNow);
|
|
_service.RaiseAlarmEvent(first);
|
|
_vm.AlarmEvents.Count.ShouldBe(1);
|
|
|
|
// Track collection-change notifications.
|
|
var changeCount = 0;
|
|
_vm.AlarmEvents.CollectionChanged += (_, _) => changeCount++;
|
|
|
|
// Now the alarm is cleared (Retain = false).
|
|
var cleared = new AlarmEventArgs(
|
|
"Source1", "HighAlarm", 500, "Cleared",
|
|
retain: false, activeState: false, ackedState: false, time: DateTime.UtcNow);
|
|
_service.RaiseAlarmEvent(cleared);
|
|
|
|
// Row must be removed.
|
|
_vm.AlarmEvents.ShouldBeEmpty();
|
|
// Exactly one collection-change notification (Remove), not two (Replace then Remove).
|
|
changeCount.ShouldBe(1);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that a non-retained alarm that has no existing row in the collection
|
|
/// is silently dropped (not added).
|
|
/// </summary>
|
|
[Fact]
|
|
public void AlarmEvent_NonRetained_WithNoExistingRow_IsNotAdded()
|
|
{
|
|
var nonRetained = new AlarmEventArgs(
|
|
"Source1", "HighAlarm", 500, "Already cleared",
|
|
retain: false, activeState: false, ackedState: false, time: DateTime.UtcNow);
|
|
_service.RaiseAlarmEvent(nonRetained);
|
|
|
|
_vm.AlarmEvents.ShouldBeEmpty();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that ActiveAlarmCount is updated correctly when an alarm is acknowledged
|
|
/// via an event update (AckedState changes from false to true).
|
|
/// </summary>
|
|
[Fact]
|
|
public void AlarmEvent_AcknowledgedUpdate_DecrementsActiveAlarmCount()
|
|
{
|
|
// Seed an unacknowledged active alarm.
|
|
var active = new AlarmEventArgs(
|
|
"Source1", "HighAlarm", 500, "Active",
|
|
retain: true, activeState: true, ackedState: false, time: DateTime.UtcNow);
|
|
_service.RaiseAlarmEvent(active);
|
|
_vm.ActiveAlarmCount.ShouldBe(1);
|
|
|
|
// Acknowledge the alarm.
|
|
var acked = new AlarmEventArgs(
|
|
"Source1", "HighAlarm", 500, "Acked",
|
|
retain: true, activeState: true, ackedState: true, time: DateTime.UtcNow);
|
|
_service.RaiseAlarmEvent(acked);
|
|
|
|
_vm.AlarmEvents.Count.ShouldBe(1);
|
|
_vm.AlarmEvents[0].AckedState.ShouldBeTrue();
|
|
_vm.ActiveAlarmCount.ShouldBe(0);
|
|
}
|
|
|
|
/// <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");
|
|
}
|
|
|
|
/// <summary>Builds a representative alarm row for shelve/confirm tests.</summary>
|
|
/// <param name="ackedState">Whether the alarm is already acknowledged.</param>
|
|
/// <param name="hasEventId">Whether the row carries an EventId; <c>false</c> models a missing EventId.</param>
|
|
/// <param name="conditionNodeId">The condition node id; pass <c>null</c> to model a missing ConditionId.</param>
|
|
private static AlarmEventViewModel BuildAlarm(
|
|
bool ackedState = false,
|
|
bool hasEventId = true,
|
|
string? conditionNodeId = "ns=2;s=Cond1")
|
|
{
|
|
return new AlarmEventViewModel(
|
|
"Source1", "HighAlarm", 500, "Temperature high",
|
|
true, true, ackedState, DateTime.UtcNow,
|
|
hasEventId ? [1, 2, 3] : null, conditionNodeId);
|
|
}
|
|
|
|
// --- ShelveAlarmAsync ---
|
|
|
|
/// <summary>Verifies that a OneShot shelve calls the service and reports success.</summary>
|
|
[Fact]
|
|
public async Task ShelveAlarm_Connected_OneShot_Succeeds()
|
|
{
|
|
_vm.IsConnected = true;
|
|
var alarm = BuildAlarm();
|
|
|
|
var (ok, _) = await _vm.ShelveAlarmAsync(alarm, ShelveKind.OneShot, 0);
|
|
|
|
ok.ShouldBeTrue();
|
|
_service.ShelveCallCount.ShouldBe(1);
|
|
_service.LastShelveCall.ShouldNotBeNull();
|
|
_service.LastShelveCall!.Value.Kind.ShouldBe(ShelveKind.OneShot);
|
|
_service.LastShelveCall.Value.ConditionNodeId.ShouldBe(alarm.ConditionNodeId);
|
|
}
|
|
|
|
/// <summary>Verifies that a Timed shelve passes the duration through to the service.</summary>
|
|
[Fact]
|
|
public async Task ShelveAlarm_Connected_Timed_PassesDuration()
|
|
{
|
|
_vm.IsConnected = true;
|
|
var alarm = BuildAlarm();
|
|
|
|
var (ok, _) = await _vm.ShelveAlarmAsync(alarm, ShelveKind.Timed, 300);
|
|
|
|
ok.ShouldBeTrue();
|
|
_service.LastShelveCall.ShouldNotBeNull();
|
|
_service.LastShelveCall!.Value.Kind.ShouldBe(ShelveKind.Timed);
|
|
_service.LastShelveCall.Value.ShelvingTimeSeconds.ShouldBe(300);
|
|
}
|
|
|
|
/// <summary>Verifies that an Unshelve calls the service and reports success.</summary>
|
|
[Fact]
|
|
public async Task ShelveAlarm_Connected_Unshelve_Succeeds()
|
|
{
|
|
_vm.IsConnected = true;
|
|
var alarm = BuildAlarm();
|
|
|
|
var (ok, _) = await _vm.ShelveAlarmAsync(alarm, ShelveKind.Unshelve, 0);
|
|
|
|
ok.ShouldBeTrue();
|
|
_service.LastShelveCall.ShouldNotBeNull();
|
|
_service.LastShelveCall!.Value.Kind.ShouldBe(ShelveKind.Unshelve);
|
|
}
|
|
|
|
/// <summary>Verifies that a Timed shelve with a non-positive duration is rejected without a service call.</summary>
|
|
[Fact]
|
|
public async Task ShelveAlarm_Timed_NonPositiveDuration_Fails_NoServiceCall()
|
|
{
|
|
_vm.IsConnected = true;
|
|
var alarm = BuildAlarm();
|
|
|
|
var (ok, _) = await _vm.ShelveAlarmAsync(alarm, ShelveKind.Timed, 0);
|
|
|
|
ok.ShouldBeFalse();
|
|
_service.ShelveCallCount.ShouldBe(0);
|
|
}
|
|
|
|
/// <summary>Verifies that shelving when disconnected is rejected without a service call.</summary>
|
|
[Fact]
|
|
public async Task ShelveAlarm_NotConnected_Fails_NoServiceCall()
|
|
{
|
|
_vm.IsConnected = false;
|
|
var alarm = BuildAlarm();
|
|
|
|
var (ok, _) = await _vm.ShelveAlarmAsync(alarm, ShelveKind.OneShot, 0);
|
|
|
|
ok.ShouldBeFalse();
|
|
_service.ShelveCallCount.ShouldBe(0);
|
|
}
|
|
|
|
/// <summary>Verifies that shelving an alarm without a ConditionNodeId is rejected without a service call.</summary>
|
|
[Fact]
|
|
public async Task ShelveAlarm_MissingConditionNodeId_Fails_NoServiceCall()
|
|
{
|
|
_vm.IsConnected = true;
|
|
var alarm = BuildAlarm(conditionNodeId: null);
|
|
|
|
var (ok, _) = await _vm.ShelveAlarmAsync(alarm, ShelveKind.OneShot, 0);
|
|
|
|
ok.ShouldBeFalse();
|
|
_service.ShelveCallCount.ShouldBe(0);
|
|
}
|
|
|
|
/// <summary>Verifies that a Bad status code from the service surfaces a "Shelve failed" message.</summary>
|
|
[Fact]
|
|
public async Task ShelveAlarm_ServiceBad_Fails_WithMessage()
|
|
{
|
|
_vm.IsConnected = true;
|
|
_service.ShelveResult = StatusCodes.BadNodeIdUnknown;
|
|
var alarm = BuildAlarm();
|
|
|
|
var (ok, message) = await _vm.ShelveAlarmAsync(alarm, ShelveKind.OneShot, 0);
|
|
|
|
ok.ShouldBeFalse();
|
|
message.ShouldContain("Shelve failed");
|
|
}
|
|
|
|
// --- ConfirmAlarmAsync ---
|
|
|
|
/// <summary>Verifies that confirming an alarm calls the service with the supplied comment.</summary>
|
|
[Fact]
|
|
public async Task ConfirmAlarm_Connected_Succeeds_PassesComment()
|
|
{
|
|
_vm.IsConnected = true;
|
|
var alarm = BuildAlarm();
|
|
|
|
var (ok, _) = await _vm.ConfirmAlarmAsync(alarm, "operator note");
|
|
|
|
ok.ShouldBeTrue();
|
|
_service.ConfirmCallCount.ShouldBe(1);
|
|
_service.LastConfirmCall.ShouldNotBeNull();
|
|
_service.LastConfirmCall!.Value.Comment.ShouldBe("operator note");
|
|
}
|
|
|
|
/// <summary>Verifies that confirming when disconnected is rejected without a service call.</summary>
|
|
[Fact]
|
|
public async Task ConfirmAlarm_NotConnected_Fails_NoServiceCall()
|
|
{
|
|
_vm.IsConnected = false;
|
|
var alarm = BuildAlarm();
|
|
|
|
var (ok, _) = await _vm.ConfirmAlarmAsync(alarm, "note");
|
|
|
|
ok.ShouldBeFalse();
|
|
_service.ConfirmCallCount.ShouldBe(0);
|
|
}
|
|
|
|
/// <summary>Verifies that confirming an alarm without an EventId is rejected without a service call.</summary>
|
|
[Fact]
|
|
public async Task ConfirmAlarm_MissingEventId_Fails_NoServiceCall()
|
|
{
|
|
_vm.IsConnected = true;
|
|
var alarm = BuildAlarm(hasEventId: false);
|
|
|
|
var (ok, _) = await _vm.ConfirmAlarmAsync(alarm, "note");
|
|
|
|
ok.ShouldBeFalse();
|
|
_service.ConfirmCallCount.ShouldBe(0);
|
|
}
|
|
|
|
/// <summary>Verifies that a Bad status code from the service surfaces a "Confirm failed" message.</summary>
|
|
[Fact]
|
|
public async Task ConfirmAlarm_ServiceBad_Fails_WithMessage()
|
|
{
|
|
_vm.IsConnected = true;
|
|
_service.ConfirmResult = StatusCodes.BadNodeIdUnknown;
|
|
var alarm = BuildAlarm();
|
|
|
|
var (ok, message) = await _vm.ConfirmAlarmAsync(alarm, "note");
|
|
|
|
ok.ShouldBeFalse();
|
|
message.ShouldContain("Confirm failed");
|
|
}
|
|
|
|
/// <summary>Verifies that confirming an alarm without a ConditionNodeId is rejected without a service call.</summary>
|
|
[Fact]
|
|
public async Task ConfirmAlarm_MissingConditionNodeId_Fails_NoServiceCall()
|
|
{
|
|
_vm.IsConnected = true;
|
|
var alarm = BuildAlarm(conditionNodeId: null);
|
|
|
|
var (ok, _) = await _vm.ConfirmAlarmAsync(alarm, "note");
|
|
|
|
ok.ShouldBeFalse();
|
|
_service.ConfirmCallCount.ShouldBe(0);
|
|
}
|
|
|
|
/// <summary>Verifies that a transport exception from ShelveAlarmAsync is caught and surfaced as an error message.</summary>
|
|
[Fact]
|
|
public async Task ShelveAlarm_ServiceThrows_ReturnsError()
|
|
{
|
|
_vm.IsConnected = true;
|
|
_service.ShelveException = new InvalidOperationException("transport error");
|
|
var alarm = BuildAlarm();
|
|
|
|
var (ok, message) = await _vm.ShelveAlarmAsync(alarm, ShelveKind.OneShot, 0);
|
|
|
|
ok.ShouldBeFalse();
|
|
message.ShouldContain("Error");
|
|
}
|
|
|
|
/// <summary>Verifies that a transport exception from ConfirmAlarmAsync is caught and surfaced as an error message.</summary>
|
|
[Fact]
|
|
public async Task ConfirmAlarm_ServiceThrows_ReturnsError()
|
|
{
|
|
_vm.IsConnected = true;
|
|
_service.ConfirmException = new InvalidOperationException("transport error");
|
|
var alarm = BuildAlarm();
|
|
|
|
var (ok, message) = await _vm.ConfirmAlarmAsync(alarm, "note");
|
|
|
|
ok.ShouldBeFalse();
|
|
message.ShouldContain("Error");
|
|
}
|
|
|
|
// --- CanShelve / CanConfirm predicates ---
|
|
|
|
/// <summary>Verifies CanShelve is true when a ConditionNodeId is present.</summary>
|
|
[Fact]
|
|
public void CanShelve_True_WhenConditionNodeIdPresent()
|
|
{
|
|
BuildAlarm(conditionNodeId: "ns=2;s=Cond1").CanShelve.ShouldBeTrue();
|
|
}
|
|
|
|
/// <summary>Verifies CanShelve is false when the ConditionNodeId is missing.</summary>
|
|
[Fact]
|
|
public void CanShelve_False_WhenConditionNodeIdNull()
|
|
{
|
|
BuildAlarm(conditionNodeId: null).CanShelve.ShouldBeFalse();
|
|
}
|
|
|
|
/// <summary>Verifies CanConfirm is true when acked with EventId and ConditionNodeId present.</summary>
|
|
[Fact]
|
|
public void CanConfirm_True_WhenAckedWithEventIdAndConditionNodeId()
|
|
{
|
|
BuildAlarm(ackedState: true).CanConfirm.ShouldBeTrue();
|
|
}
|
|
|
|
/// <summary>Verifies CanConfirm is false when the alarm has not been acknowledged.</summary>
|
|
[Fact]
|
|
public void CanConfirm_False_WhenNotAcked()
|
|
{
|
|
BuildAlarm(ackedState: false).CanConfirm.ShouldBeFalse();
|
|
}
|
|
|
|
/// <summary>Verifies CanConfirm is false when the EventId is missing.</summary>
|
|
[Fact]
|
|
public void CanConfirm_False_WhenEventIdNull()
|
|
{
|
|
BuildAlarm(ackedState: true, hasEventId: false).CanConfirm.ShouldBeFalse();
|
|
}
|
|
|
|
/// <summary>Verifies CanConfirm is false when the ConditionNodeId is missing.</summary>
|
|
[Fact]
|
|
public void CanConfirm_False_WhenConditionNodeIdNull()
|
|
{
|
|
BuildAlarm(ackedState: true, conditionNodeId: null).CanConfirm.ShouldBeFalse();
|
|
}
|
|
} |