docs+ui: backfill XML doc comments and finish dashboard layout pass

Adds missing <summary>/<param> XML docs across 99 server, worker, and test
files so CommentChecker reports zero issues (TreatWarningsAsErrors needs the
analyzer clean). Bundles in WIP dashboard work: NavSection extraction,
MainLayout/site.css/js styling alignment, and DashboardOptions/Auth tweaks.
This commit is contained in:
Joseph Doherty
2026-05-27 14:20:10 -04:00
parent 382861c602
commit 615b487a77
110 changed files with 1473 additions and 192 deletions
@@ -16,11 +16,14 @@ public sealed class AlarmClientDiscoveryTests
{
private readonly ITestOutputHelper output;
/// <summary>Initializes a new instance of the AlarmClientDiscoveryTests class.</summary>
/// <param name="output">The xUnit test output helper.</param>
public AlarmClientDiscoveryTests(ITestOutputHelper output)
{
this.output = output;
}
/// <summary>Dumps the public surface of the aaAlarmManagedClient assembly.</summary>
[Fact(Skip = "Discovery probe — flip Skip=null to dump aaAlarmManagedClient surface")]
public void DumpAlarmClientPublicSurface()
{
@@ -1094,12 +1094,16 @@ public sealed class WorkerPipeSessionTests
}
/// <summary>Records an informational log event.</summary>
/// <param name="eventName">The event name.</param>
/// <param name="fields">The event fields.</param>
public void Information(string eventName, IReadOnlyDictionary<string, object?> fields)
{
Record(eventName, fields);
}
/// <summary>Records an error log event.</summary>
/// <param name="eventName">The event name.</param>
/// <param name="fields">The event fields.</param>
public void Error(string eventName, IReadOnlyDictionary<string, object?> fields)
{
Record(eventName, fields);
@@ -22,6 +22,7 @@ public sealed class AlarmCommandExecutorTests
private const string SessionId = "S";
private const string CorrelationId = "C";
/// <summary>Verifies that the handler routes alarm subscriptions and returns ok.</summary>
[Fact]
public void SubscribeAlarms_WithHandler_RoutesToHandlerAndReturnsOk()
{
@@ -46,6 +47,7 @@ public sealed class AlarmCommandExecutorTests
Assert.Equal(SessionId, handler.LastSessionId);
}
/// <summary>Verifies that subscription without handler returns invalid request.</summary>
[Fact]
public void SubscribeAlarms_WithoutHandler_ReturnsInvalidRequest()
{
@@ -67,6 +69,7 @@ public sealed class AlarmCommandExecutorTests
Assert.Equal(ProtocolStatusCode.InvalidRequest, reply.ProtocolStatus.Code);
}
/// <summary>Verifies that empty subscription expression returns invalid request.</summary>
[Fact]
public void SubscribeAlarms_WithEmptyExpression_ReturnsInvalidRequest()
{
@@ -88,6 +91,7 @@ public sealed class AlarmCommandExecutorTests
Assert.Equal(ProtocolStatusCode.InvalidRequest, reply.ProtocolStatus.Code);
}
/// <summary>Verifies that acknowledge routes native status into hresult and payload.</summary>
[Fact]
public void AcknowledgeAlarm_WithHandler_RoutesNativeStatusIntoHresultAndPayload()
{
@@ -121,6 +125,7 @@ public sealed class AlarmCommandExecutorTests
Assert.Equal("alice", handler.LastAckOperatorName);
}
/// <summary>Verifies that invalid alarm GUID returns invalid request.</summary>
[Fact]
public void AcknowledgeAlarm_WithInvalidGuid_ReturnsInvalidRequest()
{
@@ -142,6 +147,7 @@ public sealed class AlarmCommandExecutorTests
Assert.Equal(ProtocolStatusCode.InvalidRequest, reply.ProtocolStatus.Code);
}
/// <summary>Verifies that nonzero native status carries a diagnostic message.</summary>
[Fact]
public void AcknowledgeAlarm_WithNonzeroNativeStatus_CarriesDiagnostic()
{
@@ -165,6 +171,7 @@ public sealed class AlarmCommandExecutorTests
Assert.Contains("-123", reply.DiagnosticMessage);
}
/// <summary>Verifies that acknowledge by name routes tuple to handler.</summary>
[Fact]
public void AcknowledgeAlarmByName_WithHandler_RoutesTupleToHandler()
{
@@ -198,6 +205,7 @@ public sealed class AlarmCommandExecutorTests
Assert.Equal("alice", handler.LastAckOperatorName);
}
/// <summary>Verifies that empty alarm name returns invalid request.</summary>
[Fact]
public void AcknowledgeAlarmByName_WithEmptyName_ReturnsInvalidRequest()
{
@@ -221,6 +229,7 @@ public sealed class AlarmCommandExecutorTests
Assert.Equal(ProtocolStatusCode.InvalidRequest, reply.ProtocolStatus.Code);
}
/// <summary>Verifies that query active alarms returns payload with snapshots.</summary>
[Fact]
public void QueryActiveAlarms_WithHandler_ReturnsPayloadWithSnapshots()
{
@@ -253,6 +262,7 @@ public sealed class AlarmCommandExecutorTests
Assert.Equal("Galaxy!A", handler.LastFilterPrefix);
}
/// <summary>Verifies that unsubscribe routes to handler.</summary>
[Fact]
public void UnsubscribeAlarms_WithHandler_RoutesToHandler()
{
@@ -273,6 +283,7 @@ public sealed class AlarmCommandExecutorTests
Assert.True(handler.UnsubscribeCalled);
}
/// <summary>Verifies that unsubscribe without handler is an ok noop.</summary>
[Fact]
public void UnsubscribeAlarms_WithoutHandler_IsOkNoop()
{
@@ -291,6 +302,7 @@ public sealed class AlarmCommandExecutorTests
Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
}
/// <summary>Verifies that handler exception returns MXAccess failure status.</summary>
[Fact]
public void AcknowledgeAlarm_WhenHandlerThrows_ReturnsMxaccessFailure()
{
@@ -331,28 +343,56 @@ public sealed class AlarmCommandExecutorTests
private sealed class FakeAlarmHandler : IAlarmCommandHandler
{
/// <summary>Gets the last subscription expression.</summary>
public string? LastSubscription { get; private set; }
/// <summary>Gets the last session ID.</summary>
public string? LastSessionId { get; private set; }
/// <summary>Gets a value indicating whether unsubscribe was called.</summary>
public bool UnsubscribeCalled { get; private set; }
/// <summary>Gets the last acknowledge alarm GUID.</summary>
public Guid LastAckGuid { get; private set; }
/// <summary>Gets the last acknowledge operator name.</summary>
public string? LastAckOperatorName { get; private set; }
/// <summary>Gets or sets the value returned by acknowledge.</summary>
public int AcknowledgeReturn { get; set; }
/// <summary>Gets or sets a value indicating whether acknowledge throws.</summary>
public bool AcknowledgeThrow { get; set; }
/// <summary>Gets or sets the query result snapshots.</summary>
public IReadOnlyList<ActiveAlarmSnapshot> QueryResult { get; set; } =
Array.Empty<ActiveAlarmSnapshot>();
/// <summary>Gets the last alarm filter prefix.</summary>
public string? LastFilterPrefix { get; private set; }
/// <summary>Records a subscription.</summary>
/// <param name="subscription">The subscription expression.</param>
/// <param name="sessionId">The session identifier.</param>
public void Subscribe(string subscription, string sessionId)
{
LastSubscription = subscription;
LastSessionId = sessionId;
}
/// <summary>Records an unsubscribe request.</summary>
public void Unsubscribe()
{
UnsubscribeCalled = true;
}
/// <summary>Records an acknowledge request.</summary>
/// <param name="alarmGuid">The alarm identifier.</param>
/// <param name="comment">The acknowledge comment.</param>
/// <param name="operatorUser">The operator user name.</param>
/// <param name="operatorNode">The operator node name.</param>
/// <param name="operatorDomain">The operator domain.</param>
/// <param name="operatorFullName">The operator full name.</param>
public int Acknowledge(
Guid alarmGuid, string comment, string operatorUser,
string operatorNode, string operatorDomain, string operatorFullName)
@@ -366,6 +406,15 @@ public sealed class AlarmCommandExecutorTests
return AcknowledgeReturn;
}
/// <summary>Records an acknowledge by name request.</summary>
/// <param name="alarmName">The alarm name.</param>
/// <param name="providerName">The provider name.</param>
/// <param name="groupName">The group name.</param>
/// <param name="comment">The acknowledge comment.</param>
/// <param name="operatorUser">The operator user name.</param>
/// <param name="operatorNode">The operator node name.</param>
/// <param name="operatorDomain">The operator domain.</param>
/// <param name="operatorFullName">The operator full name.</param>
public int AcknowledgeByName(
string alarmName, string providerName, string groupName,
string comment, string operatorUser, string operatorNode,
@@ -376,21 +425,27 @@ public sealed class AlarmCommandExecutorTests
return AcknowledgeReturn;
}
/// <summary>Gets the last acknowledge by name tuple.</summary>
public (string Name, string Provider, string Group)? LastAckByNameTuple { get; private set; }
/// <summary>Queries the active alarms with the given filter prefix.</summary>
/// <param name="alarmFilterPrefix">The alarm filter prefix for the query.</param>
public IReadOnlyList<ActiveAlarmSnapshot> QueryActive(string? alarmFilterPrefix)
{
LastFilterPrefix = alarmFilterPrefix;
return QueryResult;
}
/// <summary>Gets the number of poll calls.</summary>
public int PollCount { get; private set; }
/// <summary>Increments the poll count.</summary>
public void PollOnce()
{
PollCount++;
}
/// <inheritdoc />
public void Dispose() { }
}
}
@@ -12,6 +12,7 @@ namespace ZB.MOM.WW.MxGateway.Worker.Tests.MxAccess;
/// </summary>
public sealed class AlarmCommandHandlerTests
{
/// <summary>Verifies that subscribe creates a consumer and forwards the subscription when not yet subscribed.</summary>
[Fact]
public void Subscribe_WhenNotYetSubscribed_CreatesConsumerAndCallsSubscribe()
{
@@ -26,6 +27,7 @@ public sealed class AlarmCommandHandlerTests
Assert.Equal(@"\\HOST\Galaxy!Area", consumer.LastSubscription);
}
/// <summary>Verifies that subscribe throws when already subscribed.</summary>
[Fact]
public void Subscribe_WhenAlreadySubscribed_Throws()
{
@@ -67,6 +69,7 @@ public sealed class AlarmCommandHandlerTests
Assert.True(consumer.Disposed);
}
/// <summary>Verifies that unsubscribe disposes consumer and clears state when subscribed.</summary>
[Fact]
public void Unsubscribe_WhenSubscribed_DisposesConsumerAndClearsState()
{
@@ -82,6 +85,7 @@ public sealed class AlarmCommandHandlerTests
Assert.True(consumer.Disposed);
}
/// <summary>Verifies that unsubscribe is a no-op when not yet subscribed.</summary>
[Fact]
public void Unsubscribe_WithoutPriorSubscribe_IsNoop()
{
@@ -92,6 +96,7 @@ public sealed class AlarmCommandHandlerTests
Assert.False(handler.IsSubscribed);
}
/// <summary>Verifies that acknowledge forwards to consumer with full operator identity when subscribed.</summary>
[Fact]
public void Acknowledge_WhenSubscribed_ForwardsToConsumerWithFullOperatorIdentity()
{
@@ -109,6 +114,7 @@ public sealed class AlarmCommandHandlerTests
Assert.Equal("u", consumer.LastAckOperatorName);
}
/// <summary>Verifies that acknowledge throws invalid operation when called before subscribe.</summary>
[Fact]
public void Acknowledge_BeforeSubscribe_ThrowsInvalidOperation()
{
@@ -120,6 +126,7 @@ public sealed class AlarmCommandHandlerTests
() => handler.Acknowledge(Guid.Empty, "", "", "", "", ""));
}
/// <summary>Verifies that query active returns mapped proto snapshots when consumer has alarms.</summary>
[Fact]
public void QueryActive_WhenConsumerHasAlarms_ReturnsMappedProtoSnapshots()
{
@@ -151,6 +158,7 @@ public sealed class AlarmCommandHandlerTests
Assert.Equal(AlarmConditionState.Active, snapshots[0].CurrentState);
}
/// <summary>Verifies that query active filters by prefix when prefix is provided.</summary>
[Fact]
public void QueryActive_WithPrefix_FiltersByPrefix()
{
@@ -173,6 +181,7 @@ public sealed class AlarmCommandHandlerTests
Assert.Equal("Galaxy!AreaA.Tag1", filtered[0].AlarmFullReference);
}
/// <summary>Verifies that dispose unsubscribes and disposes consumer when subscribed.</summary>
[Fact]
public void Dispose_WhenSubscribed_UnsubscribesAndDisposesConsumer()
{
@@ -281,18 +290,28 @@ public sealed class AlarmCommandHandlerTests
private sealed class FakeConsumer : IMxAccessAlarmConsumer
{
#pragma warning disable CS0067 // Event never invoked — fake; AlarmCommandHandler tests don't drive transitions.
/// <summary>Emitted when an alarm state transition occurs.</summary>
public event EventHandler<MxAlarmTransitionEvent>? AlarmTransitionEmitted;
#pragma warning restore CS0067
/// <summary>Gets the last subscription request.</summary>
public string? LastSubscription { get; private set; }
/// <summary>Gets the last acknowledged alarm GUID.</summary>
public Guid LastAckGuid { get; private set; }
/// <summary>Gets the last acknowledged operator name.</summary>
public string? LastAckOperatorName { get; private set; }
/// <summary>Gets or sets the return value for acknowledge operations.</summary>
public int AcknowledgeReturn { get; set; }
/// <summary>Gets or sets the snapshot result to return.</summary>
public IReadOnlyList<MxAlarmSnapshotRecord> SnapshotResult { get; set; } =
Array.Empty<MxAlarmSnapshotRecord>();
/// <summary>Gets or sets a value indicating whether to throw on subscribe.</summary>
public bool ThrowOnSubscribe { get; set; }
/// <summary>Gets a value indicating whether the consumer has been disposed.</summary>
public bool Disposed { get; private set; }
/// <summary>Subscribes to alarms with the given subscription string.</summary>
/// <param name="subscription">The subscription reference.</param>
public void Subscribe(string subscription)
{
LastSubscription = subscription;
@@ -302,6 +321,13 @@ public sealed class AlarmCommandHandlerTests
}
}
/// <summary>Acknowledges an alarm by GUID.</summary>
/// <param name="alarmGuid">The alarm GUID.</param>
/// <param name="ackComment">The acknowledgment comment.</param>
/// <param name="ackOperatorName">The operator name.</param>
/// <param name="ackOperatorNode">The operator node.</param>
/// <param name="ackOperatorDomain">The operator domain.</param>
/// <param name="ackOperatorFullName">The operator full name.</param>
public int AcknowledgeByGuid(
Guid alarmGuid, string ackComment, string ackOperatorName,
string ackOperatorNode, string ackOperatorDomain, string ackOperatorFullName)
@@ -311,6 +337,15 @@ public sealed class AlarmCommandHandlerTests
return AcknowledgeReturn;
}
/// <summary>Acknowledges an alarm by name.</summary>
/// <param name="alarmName">The alarm name.</param>
/// <param name="providerName">The provider name.</param>
/// <param name="groupName">The alarm group name.</param>
/// <param name="ackComment">The acknowledgment comment.</param>
/// <param name="ackOperatorName">The operator name.</param>
/// <param name="ackOperatorNode">The operator node.</param>
/// <param name="ackOperatorDomain">The operator domain.</param>
/// <param name="ackOperatorFullName">The operator full name.</param>
public int AcknowledgeByName(
string alarmName, string providerName, string groupName,
string ackComment, string ackOperatorName, string ackOperatorNode,
@@ -321,17 +356,22 @@ public sealed class AlarmCommandHandlerTests
return AcknowledgeReturn;
}
/// <summary>Gets the last acknowledge-by-name parameters.</summary>
public (string Name, string Provider, string Group)? LastAckByNameTuple { get; private set; }
/// <summary>Returns a snapshot of active alarms.</summary>
public IReadOnlyList<MxAlarmSnapshotRecord> SnapshotActiveAlarms() => SnapshotResult;
/// <summary>Gets the number of times polled.</summary>
public int PollCount { get; private set; }
/// <summary>Polls once for alarm updates.</summary>
public void PollOnce()
{
PollCount++;
}
/// <inheritdoc />
public void Dispose()
{
Disposed = true;
@@ -17,6 +17,7 @@ public sealed class AlarmDispatcherTests
{
private const string SessionId = "session-001";
/// <summary>Verifies that alarm transitions land in the queue with correctly mapped fields.</summary>
[Fact]
public void OnTransition_WhenAlarmTransitionRaised_LandsInQueueWithMappedFields()
{
@@ -63,6 +64,7 @@ public sealed class AlarmDispatcherTests
Assert.Equal(ts, body.TransitionTimestamp.ToDateTime());
}
/// <summary>Verifies that unchanged alarm states do not emit transitions.</summary>
[Fact]
public void OnTransition_WithConsecutiveUnchangedState_DoesNotEmitTransition()
{
@@ -89,6 +91,10 @@ public sealed class AlarmDispatcherTests
Assert.Equal(0, queue.Count);
}
/// <summary>Verifies that state transitions are mapped according to the state table.</summary>
/// <param name="previous">The previous alarm state.</param>
/// <param name="current">The current alarm state.</param>
/// <param name="expected">The expected transition kind.</param>
[Theory]
[InlineData(MxAlarmStateKind.Unspecified, MxAlarmStateKind.UnackAlm, AlarmTransitionKind.Raise)]
[InlineData(MxAlarmStateKind.UnackAlm, MxAlarmStateKind.AckAlm, AlarmTransitionKind.Acknowledge)]
@@ -122,6 +128,7 @@ public sealed class AlarmDispatcherTests
Assert.Equal(expected, evt!.Event.OnAlarmTransition.TransitionKind);
}
/// <summary>Verifies that subscribe calls are forwarded to the consumer.</summary>
[Fact]
public void Subscribe_WhenInvoked_ForwardsToConsumer()
{
@@ -135,6 +142,7 @@ public sealed class AlarmDispatcherTests
Assert.Equal(@"\\HOST\Galaxy!Area1", consumer.LastSubscription);
}
/// <summary>Verifies that acknowledge calls are forwarded to the consumer with operator identity.</summary>
[Fact]
public void Acknowledge_WhenInvoked_ForwardsToConsumerWithFullOperatorIdentity()
{
@@ -158,6 +166,7 @@ public sealed class AlarmDispatcherTests
Assert.Equal("Alice Smith", consumer.LastAckOperatorFullName);
}
/// <summary>Verifies that acknowledge-by-name calls are forwarded to the consumer.</summary>
[Fact]
public void AcknowledgeByName_WhenInvoked_ForwardsToConsumerWithFullTuple()
{
@@ -184,6 +193,7 @@ public sealed class AlarmDispatcherTests
Assert.Equal("TestArea", consumer.LastAckByNameTuple!.Value.Group);
}
/// <summary>Verifies that consumer alarm records are mapped to proto snapshots.</summary>
[Fact]
public void SnapshotActiveAlarms_WhenConsumerHasRecords_MapsRecordsToProtos()
{
@@ -232,6 +242,7 @@ public sealed class AlarmDispatcherTests
Assert.Equal(AlarmConditionState.ActiveAcked, snapshots[1].CurrentState);
}
/// <summary>Verifies that dispose unsubscribes the handler and disposes the consumer.</summary>
[Fact]
public void Dispose_WhenSubscribed_UnsubscribesHandlerAndDisposesConsumer()
{
@@ -260,30 +271,52 @@ public sealed class AlarmDispatcherTests
private sealed class FakeAlarmConsumer : IMxAccessAlarmConsumer
{
/// <summary>Raised when an alarm transition occurs.</summary>
public event EventHandler<MxAlarmTransitionEvent>? AlarmTransitionEmitted;
/// <summary>Gets the last subscription reference.</summary>
public string? LastSubscription { get; private set; }
/// <summary>Gets the GUID from the last acknowledge call.</summary>
public Guid LastAckGuid { get; private set; }
/// <summary>Gets the comment from the last acknowledge call.</summary>
public string? LastAckComment { get; private set; }
/// <summary>Gets the operator name from the last acknowledge call.</summary>
public string? LastAckOperatorName { get; private set; }
/// <summary>Gets the operator node from the last acknowledge call.</summary>
public string? LastAckOperatorNode { get; private set; }
/// <summary>Gets the operator domain from the last acknowledge call.</summary>
public string? LastAckOperatorDomain { get; private set; }
/// <summary>Gets the operator full name from the last acknowledge call.</summary>
public string? LastAckOperatorFullName { get; private set; }
/// <summary>Gets or sets the return code for acknowledge operations.</summary>
public int AcknowledgeReturn { get; set; }
/// <summary>Gets or sets the result collection for snapshot operations.</summary>
public IReadOnlyList<MxAlarmSnapshotRecord> SnapshotResult { get; set; } =
Array.Empty<MxAlarmSnapshotRecord>();
/// <summary>Gets a value indicating whether this instance has been disposed.</summary>
public bool Disposed { get; private set; }
/// <summary>Raises an alarm transition event.</summary>
/// <param name="transition">The alarm transition event.</param>
public void RaiseTransition(MxAlarmTransitionEvent transition)
{
AlarmTransitionEmitted?.Invoke(this, transition);
}
/// <summary>Records the subscription reference.</summary>
/// <param name="subscription">The subscription reference.</param>
public void Subscribe(string subscription)
{
LastSubscription = subscription;
}
/// <summary>Records an acknowledge-by-GUID call with operator identity.</summary>
/// <param name="alarmGuid">The alarm GUID.</param>
/// <param name="ackComment">The acknowledgment comment.</param>
/// <param name="ackOperatorName">The operator name.</param>
/// <param name="ackOperatorNode">The operator node.</param>
/// <param name="ackOperatorDomain">The operator domain.</param>
/// <param name="ackOperatorFullName">The operator full name.</param>
public int AcknowledgeByGuid(
Guid alarmGuid,
string ackComment,
@@ -301,6 +334,15 @@ public sealed class AlarmDispatcherTests
return AcknowledgeReturn;
}
/// <summary>Records an acknowledge-by-name call with alarm name, provider, and group.</summary>
/// <param name="alarmName">The alarm name.</param>
/// <param name="providerName">The provider name.</param>
/// <param name="groupName">The alarm group name.</param>
/// <param name="ackComment">The acknowledgment comment.</param>
/// <param name="ackOperatorName">The operator name.</param>
/// <param name="ackOperatorNode">The operator node.</param>
/// <param name="ackOperatorDomain">The operator domain.</param>
/// <param name="ackOperatorFullName">The operator full name.</param>
public int AcknowledgeByName(
string alarmName, string providerName, string groupName,
string ackComment, string ackOperatorName, string ackOperatorNode,
@@ -311,20 +353,25 @@ public sealed class AlarmDispatcherTests
return AcknowledgeReturn;
}
/// <summary>Gets the last acknowledge-by-name tuple (alarm name, provider, group).</summary>
public (string Name, string Provider, string Group)? LastAckByNameTuple { get; private set; }
/// <summary>Returns the current snapshot result collection.</summary>
public IReadOnlyList<MxAlarmSnapshotRecord> SnapshotActiveAlarms()
{
return SnapshotResult;
}
/// <summary>Gets the count of poll operations.</summary>
public int PollCount { get; private set; }
/// <summary>Increments the poll count.</summary>
public void PollOnce()
{
PollCount++;
}
/// <inheritdoc />
public void Dispose()
{
Disposed = true;
@@ -14,6 +14,7 @@ namespace ZB.MOM.WW.MxGateway.Worker.Tests.MxAccess;
/// </summary>
public sealed class AlarmRecordTransitionMapperTests
{
/// <summary>Verifies that full alarm reference uses provider!group.name format.</summary>
[Fact]
public void ComposeFullReference_WithProviderAndGroup_UsesProviderBangGroupDotNameFormat()
{
@@ -24,6 +25,7 @@ public sealed class AlarmRecordTransitionMapperTests
Assert.Equal("GalaxyAlarmProvider!Tank01.Level.HiHi", reference);
}
/// <summary>Verifies that reference omits provider when empty.</summary>
[Fact]
public void ComposeFullReference_WithEmptyProvider_DropsProvider()
{
@@ -32,6 +34,7 @@ public sealed class AlarmRecordTransitionMapperTests
Assert.Equal("Tank01.Level.HiHi", reference);
}
/// <summary>Verifies that reference omits group when empty.</summary>
[Fact]
public void ComposeFullReference_WithEmptyGroup_DropsGroup()
{
@@ -40,6 +43,7 @@ public sealed class AlarmRecordTransitionMapperTests
Assert.Equal("GalaxyAlarmProvider!GlobalAlarm", reference);
}
/// <summary>Verifies that reference returns alarm name when provider and group are empty.</summary>
[Fact]
public void ComposeFullReference_WithEmptyProviderAndGroup_ReturnsAlarmName()
{
@@ -48,6 +52,9 @@ public sealed class AlarmRecordTransitionMapperTests
Assert.Equal("Bare", reference);
}
/// <summary>Verifies that state string parsing decodes all valid state kinds.</summary>
/// <param name="input">The state string to parse.</param>
/// <param name="expected">The expected decoded state kind.</param>
[Theory]
[InlineData("UNACK_ALM", MxAlarmStateKind.UnackAlm)]
[InlineData("ACK_ALM", MxAlarmStateKind.AckAlm)]
@@ -63,6 +70,10 @@ public sealed class AlarmRecordTransitionMapperTests
Assert.Equal(expected, AlarmRecordTransitionMapper.ParseStateKind(input));
}
/// <summary>Verifies that state pair mapping decides the correct transition kind for all pairs.</summary>
/// <param name="previous">The previous alarm state kind.</param>
/// <param name="current">The current alarm state kind.</param>
/// <param name="expected">The expected transition kind.</param>
[Theory]
// First sighting: new alarm in *_ALM → Raise.
[InlineData(MxAlarmStateKind.Unspecified, MxAlarmStateKind.UnackAlm, AlarmTransitionKind.Raise)]
@@ -91,6 +102,7 @@ public sealed class AlarmRecordTransitionMapperTests
Assert.Equal(expected, AlarmRecordTransitionMapper.MapTransition(previous, current));
}
/// <summary>Verifies that valid XML fields assemble into correct UTC timestamp.</summary>
[Fact]
public void ParseTransitionTimestampUtc_WithValidXmlFields_AssemblesUtc()
{
@@ -109,6 +121,7 @@ public sealed class AlarmRecordTransitionMapperTests
Assert.Equal(709, utc.Millisecond);
}
/// <summary>Verifies that unparseable inputs return DateTime.MinValue.</summary>
[Fact]
public void ParseTransitionTimestampUtc_WithUnparseableInputs_ReturnsMinValue()
{
@@ -75,17 +75,24 @@ public sealed class MxAccessComServerTests
private readonly int registerHandle;
private readonly List<string> calls = new();
/// <summary>Initializes a new instance with the specified register handle.</summary>
/// <param name="registerHandle">The initial server handle value to return from Register.</param>
public RecordingMxAccessServer(int registerHandle)
{
this.registerHandle = registerHandle;
}
/// <summary>Gets the client name passed to the most recent Register call.</summary>
public string? RegisteredClientName { get; private set; }
/// <summary>Gets or sets an exception to throw from the Register method.</summary>
public Exception? ThrowOnRegister { get; set; }
/// <summary>Gets the recorded method calls as strings.</summary>
public IReadOnlyList<string> Calls => calls.ToArray();
/// <summary>Records a Register call and returns the configured handle.</summary>
/// <param name="clientName">The client name to record.</param>
public int Register(string clientName)
{
calls.Add($"Register:{clientName}");
@@ -98,58 +105,103 @@ public sealed class MxAccessComServerTests
return registerHandle;
}
/// <summary>Records an Unregister call.</summary>
/// <param name="serverHandle">The MXAccess server handle.</param>
public void Unregister(int serverHandle)
{
calls.Add($"Unregister:{serverHandle}");
}
/// <summary>Records an AddItem call and returns zero.</summary>
/// <param name="serverHandle">The MXAccess server handle.</param>
/// <param name="itemDefinition">The item definition string to record.</param>
public int AddItem(int serverHandle, string itemDefinition)
{
calls.Add($"AddItem:{serverHandle}:{itemDefinition}");
return 0;
}
/// <summary>Records an AddItem2 call and returns zero.</summary>
/// <param name="serverHandle">The MXAccess server handle.</param>
/// <param name="itemDefinition">The item definition string to record.</param>
/// <param name="itemContext">The item context string to record.</param>
public int AddItem2(int serverHandle, string itemDefinition, string itemContext)
{
calls.Add($"AddItem2:{serverHandle}:{itemDefinition}:{itemContext}");
return 0;
}
/// <summary>Records a RemoveItem call.</summary>
/// <param name="serverHandle">The MXAccess server handle.</param>
/// <param name="itemHandle">The MXAccess item handle.</param>
public void RemoveItem(int serverHandle, int itemHandle)
{
calls.Add($"RemoveItem:{serverHandle}:{itemHandle}");
}
/// <summary>Records an Advise call.</summary>
/// <param name="serverHandle">The MXAccess server handle.</param>
/// <param name="itemHandle">The MXAccess item handle.</param>
public void Advise(int serverHandle, int itemHandle)
{
calls.Add($"Advise:{serverHandle}:{itemHandle}");
}
/// <summary>Records an UnAdvise call.</summary>
/// <param name="serverHandle">The MXAccess server handle.</param>
/// <param name="itemHandle">The MXAccess item handle.</param>
public void UnAdvise(int serverHandle, int itemHandle)
{
calls.Add($"UnAdvise:{serverHandle}:{itemHandle}");
}
/// <summary>Records an AdviseSupervisory call.</summary>
/// <param name="serverHandle">The MXAccess server handle.</param>
/// <param name="itemHandle">The MXAccess item handle.</param>
public void AdviseSupervisory(int serverHandle, int itemHandle)
{
calls.Add($"AdviseSupervisory:{serverHandle}:{itemHandle}");
}
/// <summary>Records a Write call.</summary>
/// <param name="serverHandle">The MXAccess server handle.</param>
/// <param name="itemHandle">The MXAccess item handle.</param>
/// <param name="value">The value to write.</param>
/// <param name="userId">The user identifier.</param>
public void Write(int serverHandle, int itemHandle, object? value, int userId)
{
calls.Add($"Write:{serverHandle}:{itemHandle}:{value}:{userId}");
}
/// <summary>Records a Write2 call.</summary>
/// <param name="serverHandle">The MXAccess server handle.</param>
/// <param name="itemHandle">The MXAccess item handle.</param>
/// <param name="value">The value to write.</param>
/// <param name="timestamp">The timestamp value.</param>
/// <param name="userId">The user identifier.</param>
public void Write2(int serverHandle, int itemHandle, object? value, object? timestamp, int userId)
{
calls.Add($"Write2:{serverHandle}:{itemHandle}:{value}:{timestamp}:{userId}");
}
/// <summary>Records a WriteSecured call.</summary>
/// <param name="serverHandle">The MXAccess server handle.</param>
/// <param name="itemHandle">The MXAccess item handle.</param>
/// <param name="currentUserId">The current user identifier.</param>
/// <param name="verifierUserId">The verifier user identifier.</param>
/// <param name="value">The value to write.</param>
public void WriteSecured(int serverHandle, int itemHandle, int currentUserId, int verifierUserId, object? value)
{
calls.Add($"WriteSecured:{serverHandle}:{itemHandle}:{currentUserId}:{verifierUserId}:{value}");
}
/// <summary>Records a WriteSecured2 call.</summary>
/// <param name="serverHandle">The MXAccess server handle.</param>
/// <param name="itemHandle">The MXAccess item handle.</param>
/// <param name="currentUserId">The current user identifier.</param>
/// <param name="verifierUserId">The verifier user identifier.</param>
/// <param name="value">The value to write.</param>
/// <param name="timestamp">The timestamp value.</param>
public void WriteSecured2(
int serverHandle, int itemHandle, int currentUserId, int verifierUserId, object? value, object? timestamp)
{
@@ -1441,6 +1441,7 @@ public sealed class MxAccessCommandExecutorTests
/// <param name="adviseException">Exception to throw from Advise, if any.</param>
/// <param name="unAdviseException">Exception to throw from UnAdvise, if any.</param>
/// <param name="adviseSupervisoryException">Exception to throw from AdviseSupervisory, if any.</param>
/// <param name="writeExceptionByItemHandle">Map of item handles to exceptions thrown on write.</param>
public FakeMxAccessComObject(
int registerHandle,
int addItemHandle = 0,
@@ -227,6 +227,7 @@ public sealed class MxAccessEventMapperTests
}
/// <summary>Verifies unparseable or empty timestamp input is rejected without throwing.</summary>
/// <param name="text">Unparseable or empty timestamp string.</param>
[Theory]
[InlineData(null)]
[InlineData("")]
@@ -474,45 +474,73 @@ public sealed class MxAccessStaSessionTests
private int pollCount;
private int? lastPollThreadId;
/// <summary>Gets a value indicating whether the alarm client is currently subscribed.</summary>
public bool IsSubscribed { get; private set; }
/// <summary>Gets the last alarm subscription name.</summary>
public string? LastSubscription { get; private set; }
/// <summary>Exception thrown by PollOnce; null to succeed.</summary>
public Exception? PollException { get; set; }
/// <summary>Gets the count of PollOnce calls.</summary>
public int PollCount
{
get { lock (gate) return pollCount; }
}
/// <summary>Gets the managed thread ID of the last PollOnce call.</summary>
public int? LastPollThreadId
{
get { lock (gate) return lastPollThreadId; }
}
/// <summary>Subscribes to alarm events.</summary>
/// <param name="subscription">The subscription descriptor.</param>
/// <param name="sessionId">The session identifier.</param>
public void Subscribe(string subscription, string sessionId)
{
IsSubscribed = true;
LastSubscription = subscription;
}
/// <summary>Unsubscribes from alarm events.</summary>
public void Unsubscribe()
{
IsSubscribed = false;
}
/// <summary>Acknowledges an alarm by guid.</summary>
/// <param name="alarmGuid">The alarm GUID.</param>
/// <param name="comment">The acknowledgment comment.</param>
/// <param name="operatorUser">The operator user name.</param>
/// <param name="operatorNode">The operator node name.</param>
/// <param name="operatorDomain">The operator domain.</param>
/// <param name="operatorFullName">The operator full name.</param>
public int Acknowledge(Guid alarmGuid, string comment, string operatorUser,
string operatorNode, string operatorDomain, string operatorFullName)
=> 0;
/// <summary>Acknowledges an alarm by name.</summary>
/// <param name="alarmName">The alarm name.</param>
/// <param name="providerName">The provider name.</param>
/// <param name="groupName">The alarm group name.</param>
/// <param name="comment">The acknowledgment comment.</param>
/// <param name="operatorUser">The operator user name.</param>
/// <param name="operatorNode">The operator node name.</param>
/// <param name="operatorDomain">The operator domain.</param>
/// <param name="operatorFullName">The operator full name.</param>
public int AcknowledgeByName(string alarmName, string providerName, string groupName,
string comment, string operatorUser, string operatorNode,
string operatorDomain, string operatorFullName)
=> 0;
/// <summary>Queries active alarms.</summary>
/// <param name="alarmFilterPrefix">Optional alarm name filter prefix.</param>
public IReadOnlyList<ActiveAlarmSnapshot> QueryActive(string? alarmFilterPrefix)
=> Array.Empty<ActiveAlarmSnapshot>();
/// <summary>Polls for alarm events once.</summary>
public void PollOnce()
{
lock (gate)
@@ -527,6 +555,7 @@ public sealed class MxAccessStaSessionTests
}
}
/// <inheritdoc />
public void Dispose() { }
}
}
@@ -17,6 +17,7 @@ namespace ZB.MOM.WW.MxGateway.Worker.Tests.MxAccess;
/// </summary>
public sealed class MxAccessValueCacheTests
{
/// <summary>Verifies that cache returns the last value with incrementing versions.</summary>
[Fact]
public void Set_ThenTryGet_ReturnsLastValueWithIncrementingVersion()
{
@@ -45,6 +46,7 @@ public sealed class MxAccessValueCacheTests
Assert.Equal(999, other.Value.Int32Value);
}
/// <summary>Verifies that TryGet returns false for unknown handles.</summary>
[Fact]
public void TryGet_WithUnknownHandle_ReturnsFalse()
{
@@ -53,6 +55,7 @@ public sealed class MxAccessValueCacheTests
Assert.False(cache.TryGet(serverHandle: 7, itemHandle: 21, out _));
}
/// <summary>Verifies that Remove drops entries and resets versions.</summary>
[Fact]
public void Remove_DropsEntryAndResetsVersion()
{
@@ -71,6 +74,7 @@ public sealed class MxAccessValueCacheTests
Assert.Equal(1UL, reset.Version);
}
/// <summary>Verifies that CurrentVersion returns zero for unknown handles and the latest for known ones.</summary>
[Fact]
public void CurrentVersion_ReturnsZeroForUnknown_AndLatestForKnown()
{
@@ -83,23 +87,7 @@ public sealed class MxAccessValueCacheTests
Assert.Equal(2UL, cache.CurrentVersion(7, 21));
}
/// <summary>
/// Worker.Tests-020: pins the contract that <c>TryWaitForUpdate</c>
/// returns <c>false</c> when the deadline has elapsed with no
/// <c>Set</c>, yields a default <c>CachedValue</c>, and invokes
/// <c>pumpStep</c> at least once so MXAccess Windows messages can
/// be dispatched. Earlier revisions of this test asserted both an
/// elapsed-time floor (<c>stopwatch.ElapsedMilliseconds &gt;= 60</c>)
/// and <c>pumpCalls &gt; 1</c> — the same wall-clock-floor race
/// pattern Worker.Tests-003/004/013 corrected. To eliminate the
/// timing dependency entirely (the equivalent of a manual time
/// source for a <c>DateTime.UtcNow</c>-based deadline), the test
/// now supplies a deadline already in the past: the loop pumps
/// once, observes the passed deadline, and returns false
/// deterministically without any <c>Thread.Sleep</c>. The
/// deadline-honouring contract is what this test exists to pin;
/// elapsed time and pump-iteration count are incidental.
/// </summary>
/// <summary>Verifies that TryWaitForUpdate returns false after the deadline expires.</summary>
[Fact]
public void TryWaitForUpdate_ReturnsFalseAfterDeadline_WhenNoSetOccurs()
{
@@ -126,6 +114,7 @@ public sealed class MxAccessValueCacheTests
Assert.Equal(1, pumpCalls);
}
/// <summary>Verifies that TryWaitForUpdate returns true when the cache is updated after the baseline.</summary>
[Fact]
public async Task TryWaitForUpdate_ReturnsTrue_WhenSetFiresAfterBaselineVersion()
{
@@ -35,6 +35,7 @@ public sealed class WnWrapAlarmConsumerXmlTests
private const string EmptyXml =
"<?xml version=\"1.0\"?><ALARM_RECORDS COUNT=\"0\"></ALARM_RECORDS>";
/// <summary>Verifies that empty XML payload returns an empty dictionary.</summary>
[Fact]
public void ParseSnapshotXml_WithEmptyPayload_ReturnsEmptyDictionary()
{
@@ -42,6 +43,7 @@ public sealed class WnWrapAlarmConsumerXmlTests
Assert.Empty(records);
}
/// <summary>Verifies that null or whitespace payload returns an empty dictionary.</summary>
[Fact]
public void ParseSnapshotXml_WithNullOrWhitespace_ReturnsEmptyDictionary()
{
@@ -49,6 +51,7 @@ public sealed class WnWrapAlarmConsumerXmlTests
Assert.Empty(WnWrapAlarmConsumer.ParseSnapshotXml(" "));
}
/// <summary>Verifies that single alarm XML payload decodes the record correctly.</summary>
[Fact]
public void ParseSnapshotXml_WithSingleActiveAlarm_DecodesRecord()
{
@@ -74,6 +77,7 @@ public sealed class WnWrapAlarmConsumerXmlTests
Assert.Equal(26, record.TransitionTimestampUtc.Minute);
}
/// <summary>Verifies that invalid GUIDs in XML payload are silently dropped.</summary>
[Fact]
public void ParseSnapshotXml_WithInvalidGuids_SilentlyDropsRecords()
{
@@ -83,6 +87,9 @@ public sealed class WnWrapAlarmConsumerXmlTests
Assert.Empty(WnWrapAlarmConsumer.ParseSnapshotXml(xml));
}
/// <summary>Verifies that dashless 32-character hex GUIDs parse correctly.</summary>
/// <param name="hex">The dashless hex string.</param>
/// <param name="expected">The expected canonical GUID form.</param>
[Theory]
[InlineData("BCC4705395424D65BDAABCDEA6A32A73", "BCC47053-9542-4D65-BDAA-BCDEA6A32A73")]
[InlineData("00000000000000000000000000000000", "00000000-0000-0000-0000-000000000000")]
@@ -92,6 +99,8 @@ public sealed class WnWrapAlarmConsumerXmlTests
Assert.Equal(new Guid(expected), guid);
}
/// <summary>Verifies that canonical dashed GUID format is accepted.</summary>
/// <param name="canonical">The canonical GUID form.</param>
[Theory]
[InlineData("BCC47053-9542-4D65-BDAA-BCDEA6A32A73")]
public void TryParseHexGuid_WithCanonicalDashedForm_Accepts(string canonical)
@@ -100,6 +109,8 @@ public sealed class WnWrapAlarmConsumerXmlTests
Assert.Equal(new Guid(canonical), guid);
}
/// <summary>Verifies that invalid GUID inputs are rejected.</summary>
/// <param name="hex">The invalid GUID hex string.</param>
[Theory]
[InlineData(null)]
[InlineData("")]
@@ -135,11 +135,14 @@ public sealed class AlarmClientWmProbeTests : IDisposable
private IntPtr probeWindow = IntPtr.Zero;
private string? registeredClass;
/// <summary>Initializes a new instance of the test class.</summary>
/// <param name="output">The test output helper.</param>
public AlarmClientWmProbeTests(ITestOutputHelper output)
{
this.output = output;
}
/// <summary>Probes the alarm client for window message behavior (requires AVEVA installed).</summary>
[Fact(Skip = "Runtime probe — flip Skip=null on the dev rig (AVEVA installed) to capture alarm-path behavior")]
public void ProbeAlarmClient_OnDevRig_LogsAlarmWindowMessages()
{
@@ -772,6 +775,7 @@ public sealed class AlarmClientWmProbeTests : IDisposable
log.Enqueue($"[t={elapsed.Elapsed.TotalSeconds:F3}s] {line}");
}
/// <inheritdoc />
public void Dispose()
{
if (wndProcHandle.IsAllocated) wndProcHandle.Free();
@@ -36,11 +36,14 @@ public sealed class AlarmsLiveSmokeTests
private readonly Stopwatch elapsed = Stopwatch.StartNew();
private readonly ConcurrentQueue<string> log = new ConcurrentQueue<string>();
/// <summary>Initializes a new instance of the AlarmsLiveSmokeTests class.</summary>
/// <param name="output">Test output helper for logging.</param>
public AlarmsLiveSmokeTests(ITestOutputHelper output)
{
this.output = output;
}
/// <summary>Verifies the alarm pipeline raises and acknowledges alarms correctly.</summary>
[Fact(Skip = "Live dev-rig smoke test — flip Skip=null with AVEVA + the alarm flip script running. Verified working 2026-05-01.")]
public void Alarms_FullPipelineRoundTrip_RaisesAndAcknowledges()
{
@@ -46,11 +46,14 @@ public sealed class WnWrapConsumerProbeTests
private readonly ConcurrentQueue<string> log = new ConcurrentQueue<string>();
private readonly Stopwatch elapsed = Stopwatch.StartNew();
/// <summary>Initializes a new probe test with the given test output helper.</summary>
/// <param name="output">The xUnit test output helper.</param>
public WnWrapConsumerProbeTests(ITestOutputHelper output)
{
this.output = output;
}
/// <summary>Probes wnwrap consumer on dev rig and logs XML alarm stream output.</summary>
[Fact(Skip = "Runtime probe — flip Skip=null on the dev rig (AVEVA installed) to capture wnwrapConsumer XML alarm output. Verified working 2026-05-01.")]
public void ProbeWnWrapConsumer_OnDevRig_LogsXmlAlarmStream()
{