diff --git a/src/ZB.MOM.WW.MxGateway.Contracts/GatewayContractInfo.cs b/src/ZB.MOM.WW.MxGateway.Contracts/GatewayContractInfo.cs
index fac7d5c..ef698d2 100644
--- a/src/ZB.MOM.WW.MxGateway.Contracts/GatewayContractInfo.cs
+++ b/src/ZB.MOM.WW.MxGateway.Contracts/GatewayContractInfo.cs
@@ -23,7 +23,7 @@ public static class GatewayContractInfo
/// ZB.MOM.WW.MxGateway.IntegrationTests.LiveMxAccessFactAttribute and
/// ZB.MOM.WW.MxGateway.Worker.Tests.TestSupport.LiveMxAccessFactAttribute
/// so any future opt-in tweak does not silently leave one project
- /// behind — see Worker.Tests-025.
+ /// behind.
///
public const string LiveMxAccessOptInVariableName = "MXGATEWAY_RUN_LIVE_MXACCESS_TESTS";
}
diff --git a/src/ZB.MOM.WW.MxGateway.IntegrationTests/DashboardLdapLiveTests.cs b/src/ZB.MOM.WW.MxGateway.IntegrationTests/DashboardLdapLiveTests.cs
index 5d4e013..513b60b 100644
--- a/src/ZB.MOM.WW.MxGateway.IntegrationTests/DashboardLdapLiveTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.IntegrationTests/DashboardLdapLiveTests.cs
@@ -15,6 +15,7 @@ namespace ZB.MOM.WW.MxGateway.IntegrationTests;
public sealed class DashboardLdapLiveTests
{
/// Verifies that an admin user in the GwAdmin group authenticates successfully.
+ /// A task that represents the asynchronous operation.
[LiveLdapFact]
public async Task AuthenticateAsync_AdminInGwAdminGroup_Succeeds()
{
@@ -32,17 +33,13 @@ public sealed class DashboardLdapLiveTests
claim.Type == DashboardAuthenticationDefaults.LdapGroupClaimType
&& claim.Value.Contains("GwAdmin", StringComparison.OrdinalIgnoreCase));
- // IntegrationTests-023: DashboardAuthenticator builds the principal with a
- // ClaimTypes.Role claim resolved from the LDAP groups via the
- // DashboardGroupRoleMapper. The seeded GroupToRole map (GwAdmin -> Admin)
- // means the admin principal must carry Role=Admin alongside the raw LDAP-group
- // claim. A regression in the group→role mapping would fail this assertion.
Assert.Contains(result.Principal.Claims, claim =>
claim.Type == ClaimTypes.Role
&& claim.Value == DashboardRoles.Admin);
}
/// Verifies that a readonly user without GwAdmin group fails to authenticate.
+ /// A task that represents the asynchronous operation.
[LiveLdapFact]
public async Task AuthenticateAsync_ReadOnlyUserMissingGwAdminGroup_Fails()
{
@@ -59,6 +56,7 @@ public sealed class DashboardLdapLiveTests
}
/// Verifies that authentication with wrong password fails without leaking the password.
+ /// A task that represents the asynchronous operation.
[LiveLdapFact]
public async Task AuthenticateAsync_AdminWithWrongPassword_FailsWithoutLeakingPassword()
{
@@ -78,6 +76,7 @@ public sealed class DashboardLdapLiveTests
}
/// Verifies that authentication with unknown username fails.
+ /// A task that represents the asynchronous operation.
[LiveLdapFact]
public async Task AuthenticateAsync_UnknownUsername_Fails()
{
@@ -95,6 +94,7 @@ public sealed class DashboardLdapLiveTests
}
/// Verifies that authentication fails gracefully when the server is unreachable.
+ /// A task that represents the asynchronous operation.
[LiveLdapFact]
public async Task AuthenticateAsync_ServerUnreachable_FailsWithoutThrowing()
{
@@ -141,7 +141,7 @@ public sealed class DashboardLdapLiveTests
/// Builds the shared library by binding the real
/// MxGateway:Ldap configuration section the same way production does in
/// AddZbLdapAuth(configuration, "MxGateway:Ldap"), rather than hand-copying the
- /// gateway shadow LdapOptions defaults field by field (IntegrationTests-028).
+ /// gateway shadow LdapOptions defaults field by field.
/// Binding the section directly onto the shared type means the live tests exercise the
/// exact option-binding path production uses, pick up every shared field (including
/// , which governs the
diff --git a/src/ZB.MOM.WW.MxGateway.IntegrationTests/Galaxy/GalaxyRepositoryLiveTests.cs b/src/ZB.MOM.WW.MxGateway.IntegrationTests/Galaxy/GalaxyRepositoryLiveTests.cs
index 83cc963..45a7967 100644
--- a/src/ZB.MOM.WW.MxGateway.IntegrationTests/Galaxy/GalaxyRepositoryLiveTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.IntegrationTests/Galaxy/GalaxyRepositoryLiveTests.cs
@@ -7,6 +7,7 @@ namespace ZB.MOM.WW.MxGateway.IntegrationTests.Galaxy;
public sealed class GalaxyRepositoryLiveTests
{
/// Verifies that the Galaxy Repository can establish a live connection to the ZB database.
+ /// A task that represents the asynchronous operation.
[LiveGalaxyRepositoryFact]
public async Task TestConnection_AgainstZb_Succeeds()
{
@@ -18,6 +19,7 @@ public sealed class GalaxyRepositoryLiveTests
}
/// Verifies that the last deploy time can be retrieved from the ZB database.
+ /// A task that represents the asynchronous operation.
[LiveGalaxyRepositoryFact]
public async Task GetLastDeployTime_AgainstZb_ReturnsTimestamp()
{
@@ -29,6 +31,7 @@ public sealed class GalaxyRepositoryLiveTests
}
/// Verifies that the hierarchy can be retrieved from the ZB database.
+ /// A task that represents the asynchronous operation.
[LiveGalaxyRepositoryFact]
public async Task GetHierarchy_AgainstZb_ReturnsObjects()
{
@@ -46,6 +49,7 @@ public sealed class GalaxyRepositoryLiveTests
}
/// Verifies that object attributes can be retrieved from the ZB database.
+ /// A task that represents the asynchronous operation.
[LiveGalaxyRepositoryFact]
public async Task GetAttributes_AgainstZb_ReturnsAtLeastOneAttribute()
{
diff --git a/src/ZB.MOM.WW.MxGateway.IntegrationTests/IntegrationTestEnvironment.cs b/src/ZB.MOM.WW.MxGateway.IntegrationTests/IntegrationTestEnvironment.cs
index 69032da..9e438a8 100644
--- a/src/ZB.MOM.WW.MxGateway.IntegrationTests/IntegrationTestEnvironment.cs
+++ b/src/ZB.MOM.WW.MxGateway.IntegrationTests/IntegrationTestEnvironment.cs
@@ -7,8 +7,7 @@ public static class IntegrationTestEnvironment
///
/// Sourced from
/// so the env-var literal is shared with
- /// ZB.MOM.WW.MxGateway.Worker.Tests.TestSupport.LiveMxAccessFactAttribute
- /// (Worker.Tests-025).
+ /// ZB.MOM.WW.MxGateway.Worker.Tests.TestSupport.LiveMxAccessFactAttribute.
///
public const string LiveMxAccessVariableName = GatewayContractInfo.LiveMxAccessOptInVariableName;
public const string LiveMxAccessWorkerExecutableVariableName = "MXGATEWAY_LIVE_MXACCESS_WORKER_EXE";
@@ -104,7 +103,7 @@ public static class IntegrationTestEnvironment
/// when no root is found so a misconfigured run fails fast with an actionable
/// message rather than silently falling back to the current working directory
/// (which previously produced a misleading "worker exe not found" pointing at
- /// a fabricated path — see IntegrationTests-022). The
+ /// a fabricated path). The
/// environment variable
/// remains the escape hatch for unusual deployments.
///
@@ -115,7 +114,7 @@ public static class IntegrationTestEnvironment
/// ancestors above it. Tests pass an isolated boundary so the walker cannot
/// leak into ambient ancestors (a redirected TMP, a co-located checkout
/// at C:\src, an enclosing CI workspace, etc.) that would silently
- /// satisfy — see IntegrationTests-025.
+ /// satisfy .
/// Production callers pass so the walk continues to the
/// drive root as before.
///
diff --git a/src/ZB.MOM.WW.MxGateway.IntegrationTests/IntegrationTestEnvironmentTests.cs b/src/ZB.MOM.WW.MxGateway.IntegrationTests/IntegrationTestEnvironmentTests.cs
index 993de32..9d897fc 100644
--- a/src/ZB.MOM.WW.MxGateway.IntegrationTests/IntegrationTestEnvironmentTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.IntegrationTests/IntegrationTestEnvironmentTests.cs
@@ -34,7 +34,7 @@ public sealed class IntegrationTestEnvironmentTests
File.WriteAllText(Path.Combine(temporaryRoot, ".git"), "gitdir: ../.git/worktrees/test");
// Pass temporaryRoot as the stop-boundary so the walker can never leak
- // into ambient ancestors of Path.GetTempPath() (IntegrationTests-025).
+ // into ambient ancestors of Path.GetTempPath().
string repositoryRoot = IntegrationTestEnvironment.ResolveRepositoryRoot(
nestedDirectory,
stopBoundary: temporaryRoot);
@@ -54,13 +54,13 @@ public sealed class IntegrationTestEnvironmentTests
/// Verifies that
/// throws with a diagnostic message when
/// the walk exhausts without finding a repository root. The previous silent
- /// fallback to Directory.GetCurrentDirectory() masked misconfiguration
- /// (IntegrationTests-022); operators get a clear, actionable failure instead.
+ /// fallback to Directory.GetCurrentDirectory() masked misconfiguration;
+ /// operators get a clear, actionable failure instead.
/// The stopBoundary isolates the walker from ambient ancestors of
/// (a redirected TMP, a co-located checkout
/// at C:\src, etc.) that could otherwise satisfy
/// IsRepositoryRoot and make this assertion flake on contributor or CI
- /// boxes — see IntegrationTests-025.
+ /// boxes.
///
[Fact]
public void ResolveRepositoryRoot_NoMarkers_ThrowsInvalidOperationExceptionNamingStartAndMarkers()
@@ -97,9 +97,8 @@ public sealed class IntegrationTestEnvironmentTests
///
/// Verifies the stopBoundary parameter on
/// isolates the
- /// walker from ambient ancestors that happen to satisfy IsRepositoryRoot
- /// — the precise failure mode IntegrationTests-025 describes. The test
- /// deliberately constructs an outer directory that *does* carry repository-root
+ /// walker from ambient ancestors that happen to satisfy IsRepositoryRoot.
+ /// The test deliberately constructs an outer directory that *does* carry repository-root
/// markers (src/ + .git) and an inner isolated chain that does
/// not. Without the boundary the walker would happily stop at the outer
/// directory; with the boundary it must throw because the chain it can see
diff --git a/src/ZB.MOM.WW.MxGateway.IntegrationTests/TestSupport/NullDashboardEventBroadcaster.cs b/src/ZB.MOM.WW.MxGateway.IntegrationTests/TestSupport/NullDashboardEventBroadcaster.cs
index f47f773..27b1006 100644
--- a/src/ZB.MOM.WW.MxGateway.IntegrationTests/TestSupport/NullDashboardEventBroadcaster.cs
+++ b/src/ZB.MOM.WW.MxGateway.IntegrationTests/TestSupport/NullDashboardEventBroadcaster.cs
@@ -11,7 +11,7 @@ namespace ZB.MOM.WW.MxGateway.IntegrationTests.TestSupport;
/// The unit-test project owns a parallel copy under
/// ZB.MOM.WW.MxGateway.Tests/TestSupport/; IntegrationTests keeps its
/// own copy here so the two test projects stay independently buildable
-/// without a shared test-helpers project (IntegrationTests-024).
+/// without a shared test-helpers project.
///
public sealed class NullDashboardEventBroadcaster : IDashboardEventBroadcaster
{
diff --git a/src/ZB.MOM.WW.MxGateway.IntegrationTests/WorkerLiveMxAccessSmokeTests.cs b/src/ZB.MOM.WW.MxGateway.IntegrationTests/WorkerLiveMxAccessSmokeTests.cs
index 6f7b6a9..b22b1dd 100644
--- a/src/ZB.MOM.WW.MxGateway.IntegrationTests/WorkerLiveMxAccessSmokeTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.IntegrationTests/WorkerLiveMxAccessSmokeTests.cs
@@ -30,6 +30,7 @@ public sealed class WorkerLiveMxAccessSmokeTests(ITestOutputHelper output)
///
/// Verifies that a gateway session can register, add item, advise, and stream events from live MXAccess.
///
+ /// A task that represents the asynchronous operation.
[LiveMxAccessFact]
public async Task GatewaySession_WithLiveWorker_RegistersAdvisesStreamsDataAndCloses()
{
@@ -119,6 +120,7 @@ public sealed class WorkerLiveMxAccessSmokeTests(ITestOutputHelper output)
/// and that the worker emits a matching event
/// — the proof of round-trip the cross-language client e2e runner relies on.
///
+ /// A task that represents the asynchronous operation.
[LiveMxAccessFact]
public async Task GatewaySession_WithLiveWorker_WritesValueToAdvisedItem()
{
@@ -235,6 +237,7 @@ public sealed class WorkerLiveMxAccessSmokeTests(ITestOutputHelper output)
/// Verifies that an AddItem against an invalid server handle surfaces the MXAccess failure
/// without faulting the gateway transport, exercising the invalid-handle parity path.
///
+ /// A task that represents the asynchronous operation.
[LiveMxAccessFact]
public async Task GatewaySession_WithLiveWorker_InvalidHandleCommand_SurfacesFailureWithoutTransportFault()
{
@@ -293,6 +296,7 @@ public sealed class WorkerLiveMxAccessSmokeTests(ITestOutputHelper output)
/// OnDataChange events for the un-advised item. Exercises the lifecycle-ordering
/// parity CLAUDE.md singles out as a "do not synthesize" rule.
///
+ /// A task that represents the asynchronous operation.
[LiveMxAccessFact]
public async Task GatewaySession_WithLiveWorker_UnadviseRemoveItemUnregister_TeardownOrderingParity()
{
@@ -391,7 +395,7 @@ public sealed class WorkerLiveMxAccessSmokeTests(ITestOutputHelper output)
// only constrains events generated AFTER the teardown returned. So the
// "before" baseline is taken *after* a first settle window drains those
// in-flight events, not before UnAdvise was issued (which races against
- // the round-trip + STA dispatch + pipe send window — see IntegrationTests-017).
+ // the round-trip + STA dispatch + pipe send window).
//
// RecordingServerStreamWriter.Messages returns a snapshot copy under its
// own lock, so iterating after each settle window is safe without external
@@ -437,6 +441,7 @@ public sealed class WorkerLiveMxAccessSmokeTests(ITestOutputHelper output)
/// parity surface the gateway must not "fix" — the test asserts the reply kind and
/// protocol status, not a fabricated outcome.
///
+ /// A task that represents the asynchronous operation.
[LiveMxAccessFact]
public async Task GatewaySession_WithLiveWorker_WriteSecured_AuthenticatedRoundTripParity()
{
@@ -445,7 +450,7 @@ public sealed class WorkerLiveMxAccessSmokeTests(ITestOutputHelper output)
File.Exists(workerExecutablePath),
$"Live MXAccess worker executable was not found at {workerExecutablePath}. Build the worker or set {IntegrationTestEnvironment.LiveMxAccessWorkerExecutableVariableName}.");
- // IntegrationTests-019: CLAUDE.md's credential-redaction rule covers every log
+ // CLAUDE.md's credential-redaction rule covers every log
// surface the test sees, not just the reply's DiagnosticMessage. Wire a buffering
// wrapper around output and route the worker stdout/stderr echo and the gateway
// ILogger sink through it so the post-run assertion covers the accumulated test
@@ -573,6 +578,7 @@ public sealed class WorkerLiveMxAccessSmokeTests(ITestOutputHelper output)
/// assert the reply kind plus a non-INVALID_REQUEST protocol status, and log the
/// HResult for the record.
///
+ /// A task that represents the asynchronous operation.
[LiveMxAccessFact]
public async Task GatewaySession_WithLiveWorker_NewComCommands_RoundTripWithRealReplies()
{
@@ -726,6 +732,7 @@ public sealed class WorkerLiveMxAccessSmokeTests(ITestOutputHelper output)
/// to be non-empty and internally consistent (no crash, no dropped payload).
///
///
+ /// A task that represents the asynchronous operation.
[LiveMxAccessFact]
public async Task GatewaySession_WithLiveWorker_BufferedItem_AddsSetsIntervalAndAttemptsCapture()
{
@@ -908,6 +915,7 @@ public sealed class WorkerLiveMxAccessSmokeTests(ITestOutputHelper output)
/// must observe the abnormal exit, transition the session, and surface a non-empty
/// fault description rather than hanging or crashing.
///
+ /// A task that represents the asynchronous operation.
[LiveMxAccessFact]
public async Task GatewaySession_WithLiveWorker_AbnormalWorkerExit_MarksSessionFaulted()
{
@@ -981,7 +989,7 @@ public sealed class WorkerLiveMxAccessSmokeTests(ITestOutputHelper output)
// message (they all begin with "Worker"); tighten to the pipe/disconnect/
// end-of-stream classifications that match THIS path, so a regression that
// routed an unrelated fault here would surface as a test failure rather
- // than silently passing (see IntegrationTests-020). "heartbeat" is dropped
+ // than silently passing. "heartbeat" is dropped
// because HeartbeatGraceSeconds (15s) exceeds the StreamShutdownTimeout
// (10s) poll window, so a heartbeat-expired transition can never be
// observed inside this test.
@@ -991,7 +999,7 @@ public sealed class WorkerLiveMxAccessSmokeTests(ITestOutputHelper output)
$"Fault description '{observedFault}' did not match a known abnormal-exit classification "
+ "(expected 'pipe disconnected' or 'end of stream' from WorkerClient's EndOfStream path).");
- // IntegrationTests-021: also assert the StreamEvents call observed the fault
+ // Also assert the StreamEvents call observed the fault
// — the chain that puts the session into Faulted goes through ReadEventsAsync
// propagating a WorkerClientException into EventStreamService, which calls
// session.MarkFaulted. The gateway then maps the WorkerClientException to an
@@ -1037,7 +1045,7 @@ public sealed class WorkerLiveMxAccessSmokeTests(ITestOutputHelper output)
/// test fails on a silent stream-task exception (the Write parity test relies on this so
/// stream-side defects in event delivery are visible). When , all
/// cleanup exceptions are logged and swallowed so a real test-body assertion failure is not
- /// masked by a shutdown timeout (the original IntegrationTests-004 fix).
+ /// masked by a shutdown timeout.
///
private async Task ShutDownAsync(
GatewayServiceFixture fixture,
@@ -1607,6 +1615,7 @@ public sealed class WorkerLiveMxAccessSmokeTests(ITestOutputHelper output)
///
/// The session identifier.
/// The session if found; otherwise null.
+ /// if a session with the given id was found; otherwise .
public bool TryGetSession(string sessionId, [MaybeNullWhen(false)] out GatewaySession session)
{
return _registry.TryGet(sessionId, out session);
@@ -1615,6 +1624,7 @@ public sealed class WorkerLiveMxAccessSmokeTests(ITestOutputHelper output)
///
/// Disposes the fixture resources and closes all sessions.
///
+ /// A task that represents the asynchronous operation.
public async ValueTask DisposeAsync()
{
foreach (GatewaySession session in _registry.Snapshot())
@@ -1685,6 +1695,7 @@ public sealed class WorkerLiveMxAccessSmokeTests(ITestOutputHelper output)
/// Records the message and signals any pending waiter.
///
/// The message to write.
+ /// A task that represents the asynchronous operation.
public Task WriteAsync(T message)
{
lock (syncRoot)
@@ -1867,7 +1878,11 @@ public sealed class WorkerLiveMxAccessSmokeTests(ITestOutputHelper output)
return workerProcess;
}
- ///
+ ///
+ /// Waits for every recorded worker process to exit, up to the specified timeout per process.
+ ///
+ /// The maximum time to wait for each process.
+ /// A task that represents the asynchronous operation.
public async Task WaitForProcessesAsync(TimeSpan timeout)
{
foreach (TestWorkerProcess process in processes)
@@ -1947,7 +1962,7 @@ public sealed class WorkerLiveMxAccessSmokeTests(ITestOutputHelper output)
process.Kill(entireProcessTree);
}
- ///
+ /// Disposes the wrapped process.
public void Dispose()
{
process.Dispose();
@@ -1959,13 +1974,15 @@ public sealed class WorkerLiveMxAccessSmokeTests(ITestOutputHelper output)
///
private sealed class TestOutputLoggerProvider(ITestOutputHelper output) : ILoggerProvider
{
- ///
+ /// Creates a logger that writes to the test output helper for the given category.
+ /// Category name for the logger.
+ /// The created logger.
public ILogger CreateLogger(string categoryName)
{
return new TestOutputLogger(output, categoryName);
}
- ///
+ /// No resources to release; provided to satisfy .
public void Dispose()
{
}
@@ -1978,20 +1995,31 @@ public sealed class WorkerLiveMxAccessSmokeTests(ITestOutputHelper output)
ITestOutputHelper output,
string categoryName) : ILogger
{
- ///
+ /// Not supported; this test logger does not track scopes.
+ /// The type of the state to begin the scope for.
+ /// The identifier for the scope.
+ /// Always .
public IDisposable? BeginScope(TState state)
where TState : notnull
{
return null;
}
- ///
+ /// Determines whether the given log level is enabled.
+ /// The log level to check.
+ /// if is at least ; otherwise .
public bool IsEnabled(LogLevel logLevel)
{
return logLevel >= LogLevel.Information;
}
- ///
+ /// Writes the formatted log message and any exception to the test output helper.
+ /// The type of the object to be logged.
+ /// The severity of the log entry.
+ /// The event id associated with the log entry.
+ /// The entry to be logged, which can be an object or a message string.
+ /// The exception related to this entry, if any.
+ /// Function that creates the log message from and .
public void Log(
LogLevel logLevel,
EventId eventId,
@@ -2015,7 +2043,7 @@ public sealed class WorkerLiveMxAccessSmokeTests(ITestOutputHelper output)
///
/// Buffering wrapper around an that mirrors every line
/// written through it into a the test owns. The WriteSecured
- /// parity test (IntegrationTests-019) uses this to make CLAUDE.md's "passwords and
+ /// parity test uses this to make CLAUDE.md's "passwords and
/// WriteSecured payloads must never reach logs" rule a property of the entire
/// test output stream — gateway entries (echoed via
/// ), worker stdout/stderr (echoed via
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Alarms/AlarmWatchListResolver.cs b/src/ZB.MOM.WW.MxGateway.Server/Alarms/AlarmWatchListResolver.cs
index 52eb4de..6844cf0 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Alarms/AlarmWatchListResolver.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Alarms/AlarmWatchListResolver.cs
@@ -10,14 +10,6 @@ namespace ZB.MOM.WW.MxGateway.Server.Alarms;
/// and composes the per-attribute subtag item addresses from the configured
/// subtag names.
///
-// NOTE: The exact subtag names and the canonical AlarmFullReference shape
-// ("Galaxy!{area}.{reference}") are validated against a live Galaxy in the
-// Task 17 live smoke test. The config Subtags block exists precisely so these
-// names are not hard-coded here. The {area} is the alarm object's REAL Galaxy
-// area discovered via gobject.area_gobject_id (the alarm group the native
-// alarmmgr emits), giving exact reference parity with wnwrap. The configured
-// Discovery.Area/DefaultArea is only the fallback for explicit IncludeAttributes
-// entries, which carry no discovered area.
public sealed class AlarmWatchListResolver : IAlarmWatchListResolver
{
private const string ProviderLiteral = "Galaxy";
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Alarms/GatewayAlarmMonitor.cs b/src/ZB.MOM.WW.MxGateway.Server/Alarms/GatewayAlarmMonitor.cs
index 7395776..568bbb4 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Alarms/GatewayAlarmMonitor.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Alarms/GatewayAlarmMonitor.cs
@@ -912,6 +912,7 @@ public sealed class GatewayAlarmMonitor : BackgroundService, IGatewayAlarmServic
/// Determines whether the alarm reference matches this subscriber's filter.
/// The alarm reference to match.
+ /// if the reference matches this subscriber's prefix filter.
public bool Matches(string reference)
{
return prefix.Length == 0 || reference.StartsWith(prefix, StringComparison.Ordinal);
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Alarms/IGatewayAlarmService.cs b/src/ZB.MOM.WW.MxGateway.Server/Alarms/IGatewayAlarmService.cs
index 268f7a7..7231ba4 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Alarms/IGatewayAlarmService.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Alarms/IGatewayAlarmService.cs
@@ -46,6 +46,7 @@ public interface IGatewayAlarmService
///
/// Optional alarm-reference prefix scoping the feed.
/// Token that ends the subscription.
+ /// An asynchronous stream of alarm feed messages.
IAsyncEnumerable StreamAsync(
string? alarmFilterPrefix,
CancellationToken cancellationToken);
@@ -57,6 +58,7 @@ public interface IGatewayAlarmService
///
/// The acknowledge request.
/// Token to cancel the call.
+ /// The acknowledge reply, carrying the outcome in its protocol status.
Task AcknowledgeAsync(
AcknowledgeAlarmRequest request,
CancellationToken cancellationToken);
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Configuration/GatewayOptionsValidator.cs b/src/ZB.MOM.WW.MxGateway.Server/Configuration/GatewayOptionsValidator.cs
index 19b55e7..e83d784 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Configuration/GatewayOptionsValidator.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Configuration/GatewayOptionsValidator.cs
@@ -9,11 +9,7 @@ public sealed class GatewayOptionsValidator : OptionsValidatorBase
- /// Validates gateway configuration options.
- ///
- /// The accumulator to record failures on.
- /// Gateway options to validate.
+ ///
protected override void Validate(ValidationBuilder builder, GatewayOptions options)
{
ValidateAuthentication(options.Authentication, builder);
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Configuration/IGatewayConfigurationProvider.cs b/src/ZB.MOM.WW.MxGateway.Server/Configuration/IGatewayConfigurationProvider.cs
index d4a1275..32218db 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Configuration/IGatewayConfigurationProvider.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Configuration/IGatewayConfigurationProvider.cs
@@ -8,5 +8,6 @@ public interface IGatewayConfigurationProvider
///
/// Returns the validated and effective gateway configuration.
///
+ /// The validated and effective gateway configuration.
EffectiveGatewayConfiguration GetEffectiveConfiguration();
}
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Configuration/SessionOptions.cs b/src/ZB.MOM.WW.MxGateway.Server/Configuration/SessionOptions.cs
index ac2d373..f95cd55 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Configuration/SessionOptions.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Configuration/SessionOptions.cs
@@ -33,7 +33,7 @@ public sealed class SessionOptions
/// external subscriber, so a session whose only remaining subscriber is the dashboard
/// mirror still enters detach-grace. A value of 0 disables retention: the
/// session reverts to the original behavior of lingering only until its normal lease
- /// expires. The reconnect/replay itself is implemented separately (Task 12); this
+ /// expires. The reconnect/replay itself is implemented separately; this
/// option controls retention and expiry only.
///
///
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Dashboard/Components/DashboardPageBase.cs b/src/ZB.MOM.WW.MxGateway.Server/Dashboard/Components/DashboardPageBase.cs
index f323bd4..ded13c8 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Dashboard/Components/DashboardPageBase.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Dashboard/Components/DashboardPageBase.cs
@@ -38,7 +38,8 @@ public abstract class DashboardPageBase : ComponentBase, IAsyncDisposable
await ConnectHubAsync().ConfigureAwait(false);
}
- ///
+ /// Disposes the SignalR hub connection created for this page, tolerating disposal-time errors.
+ /// A task that represents the asynchronous operation.
public async ValueTask DisposeAsync()
{
if (_hub is not null)
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardApiKeyAuthorization.cs b/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardApiKeyAuthorization.cs
index cc81219..ff1890b 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardApiKeyAuthorization.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardApiKeyAuthorization.cs
@@ -6,6 +6,7 @@ public sealed class DashboardApiKeyAuthorization
{
/// Determines whether the user can manage API keys.
/// The authenticated user principal.
+ /// if the user is authenticated and holds the role; otherwise .
public bool CanManage(ClaimsPrincipal user)
{
if (user.Identity?.IsAuthenticated != true)
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardApiKeyManagementService.cs b/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardApiKeyManagementService.cs
index 86a241d..315c05a 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardApiKeyManagementService.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardApiKeyManagementService.cs
@@ -20,17 +20,13 @@ public sealed class DashboardApiKeyManagementService(
private const string UnauthorizedMessage = "Sign in with an authorized LDAP account to manage API keys.";
private const string PepperUnavailableMarker = "pepper unavailable";
- /// Determines whether the user can manage API keys.
- /// The authenticated user principal.
+ ///
public bool CanManage(ClaimsPrincipal user)
{
return authorization.CanManage(user);
}
- /// Creates an API key asynchronously.
- /// The authenticated user principal.
- /// The request payload.
- /// Token to observe for cancellation.
+ ///
public async Task CreateAsync(
ClaimsPrincipal user,
DashboardApiKeyManagementRequest request,
@@ -82,10 +78,7 @@ public sealed class DashboardApiKeyManagementService(
}
}
- /// Revokes an API key asynchronously.
- /// The authenticated user principal.
- /// The API key identifier.
- /// Token to observe for cancellation.
+ ///
public async Task RevokeAsync(
ClaimsPrincipal user,
string keyId,
@@ -120,10 +113,7 @@ public sealed class DashboardApiKeyManagementService(
: DashboardApiKeyManagementResult.Fail("API key was not found or is already revoked.");
}
- /// Rotates an API key secret asynchronously.
- /// The authenticated user principal.
- /// The API key identifier.
- /// Token to observe for cancellation.
+ ///
public async Task RotateAsync(
ClaimsPrincipal user,
string keyId,
@@ -170,10 +160,7 @@ public sealed class DashboardApiKeyManagementService(
}
}
- /// Deletes a revoked API key asynchronously.
- /// The authenticated user principal.
- /// The API key identifier.
- /// Token to observe for cancellation.
+ ///
public async Task DeleteAsync(
ClaimsPrincipal user,
string keyId,
@@ -242,15 +229,15 @@ public sealed class DashboardApiKeyManagementService(
///
/// Emits the dashboard's own canonical for a dashboard-* op
- /// directly through the best-effort (Task 2.3 #6). This is in
+ /// directly through the best-effort . This is in
/// addition to the create/revoke/rotate-key event that
/// emits via the canonical-forwarding IApiKeyAuditStore adapter — the doubled-audit
/// behaviour is preserved, both rows now land in the canonical audit_event store.
///
///
- /// Phase 3 (Actor = operator principal): Actor is the LDAP operator who performed the
+ /// Actor is the LDAP operator who performed the
/// action (resolved from the principal); Target is the managed
- /// API key id. This fixes the pre-Phase-3 semantic gap where both fields held the keyId.
+ /// API key id. This fixes an earlier semantic gap where both fields held the keyId.
///
private async Task WriteDashboardAuditAsync(
ClaimsPrincipal user,
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardAuthenticationResult.cs b/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardAuthenticationResult.cs
index b656c79..14abb73 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardAuthenticationResult.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardAuthenticationResult.cs
@@ -23,6 +23,7 @@ public sealed record DashboardAuthenticationResult(
/// Creates a successful authentication result.
///
/// Authenticated principal.
+ /// A successful wrapping .
public static DashboardAuthenticationResult Success(ClaimsPrincipal principal)
{
return new DashboardAuthenticationResult(true, principal, null);
@@ -32,6 +33,7 @@ public sealed record DashboardAuthenticationResult(
/// Creates a failed authentication result.
///
/// Diagnostic message describing the failure.
+ /// A failed carrying .
public static DashboardAuthenticationResult Fail(string failureMessage)
{
return new DashboardAuthenticationResult(false, null, failureMessage);
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardAuthenticator.cs b/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardAuthenticator.cs
index 37d78f5..4ad86fe 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardAuthenticator.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardAuthenticator.cs
@@ -16,7 +16,7 @@ namespace ZB.MOM.WW.MxGateway.Server.Dashboard;
/// shaped exactly as before (see ).
///
/// Shared LDAP bind-then-search provider.
-/// Maps LDAP groups to dashboard roles (Task 1.1 seam).
+/// Maps LDAP groups to dashboard roles.
/// Logger for diagnostic, credential-free login outcomes.
public sealed class DashboardAuthenticator(
ILdapAuthService ldapAuthService,
@@ -107,11 +107,11 @@ public sealed class DashboardAuthenticator(
[
// Keep NameIdentifier so any existing read-site that uses it continues to work.
new Claim(ClaimTypes.NameIdentifier, username),
- // Canonical login-username claim (Task 1.5).
+ // Canonical login-username claim.
new Claim(ZbClaimTypes.Username, username),
// ZbClaimTypes.Name == ClaimTypes.Name — drives Identity.Name resolution.
new Claim(ZbClaimTypes.Name, displayName),
- // Canonical display-name claim for cross-app consistency (Task 1.5).
+ // Canonical display-name claim for cross-app consistency.
new Claim(ZbClaimTypes.DisplayName, displayName),
];
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardConnectionStringDisplay.cs b/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardConnectionStringDisplay.cs
index b0f1f2d..fbdab3c 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardConnectionStringDisplay.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardConnectionStringDisplay.cs
@@ -6,6 +6,7 @@ public static class DashboardConnectionStringDisplay
{
/// Returns a sanitized Galaxy Repository connection string for display.
/// The connection string to sanitize.
+ /// A connection string with only display-safe fields, or a placeholder if it cannot be parsed.
public static string GalaxyRepositoryConnectionString(string connectionString)
{
try
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardGroupRoleMapper.cs b/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardGroupRoleMapper.cs
index 946c7a1..b9685df 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardGroupRoleMapper.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardGroupRoleMapper.cs
@@ -17,7 +17,10 @@ namespace ZB.MOM.WW.MxGateway.Server.Dashboard;
public sealed class DashboardGroupRoleMapper(IOptions options)
: IGroupRoleMapper
{
- ///
+ /// Maps the supplied dashboard LDAP group memberships to a dashboard role.
+ /// The user's directory group memberships.
+ /// A token to request cancellation of the operation.
+ /// The dashboard roles granted; scope is unused and always .
public Task> MapAsync(
IReadOnlyList groups,
CancellationToken ct)
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardGroupRoleMapping.cs b/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardGroupRoleMapping.cs
index 2fa1029..53b9d50 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardGroupRoleMapping.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardGroupRoleMapping.cs
@@ -16,6 +16,7 @@ internal static class DashboardGroupRoleMapping
///
/// The collection of LDAP groups the user belongs to.
/// The mapping from group names to dashboard role names.
+ /// The distinct dashboard roles the groups map to, or an empty list if none match.
internal static IReadOnlyList MapGroupsToRoles(
IEnumerable groups,
IReadOnlyDictionary groupToRole)
@@ -30,25 +31,6 @@ internal static class DashboardGroupRoleMapping
{
string normalizedGroup = group.Trim();
- // Lookup precedence (Server-040): the full literal group string is
- // tried first; only if that misses do we fall back to the leading
- // RDN value (e.g. "GwAdmin" extracted from
- // "ou=GwAdmin,ou=groups,..."). The map's comparer is
- // OrdinalIgnoreCase (see DashboardOptions.GroupToRole), so e.g.
- // "GwAdmin" and "gwadmin" both match.
- //
- // Review C1: with the shared ZB.MOM.WW.Auth.Ldap provider, groups
- // arrive here already stripped to short RDN names (the library calls
- // FirstRdnValue before returning them). So through the live login path
- // the full-string branch only ever sees short names and the RDN
- // fallback is effectively a no-op — they collapse to the same key.
- // The fallback is retained because this mapping is also reachable
- // directly via the IGroupRoleMapper seam (DashboardGroupRoleMapper),
- // where a caller could still pass a full DN. CONSEQUENCE: configuring a
- // full-DN GroupToRole *key* (e.g. "ou=GwAdmin,ou=groups,...") is
- // UNSUPPORTED with the shared library — the incoming group is a short
- // name, so it will never equal a full-DN key. Keep GroupToRole keys as
- // short group names.
if (groupToRole.TryGetValue(normalizedGroup, out string? mapped)
|| groupToRole.TryGetValue(ExtractFirstRdnValue(normalizedGroup), out mapped))
{
@@ -61,6 +43,7 @@ internal static class DashboardGroupRoleMapping
/// Extracts the first RDN value from a distinguished name.
/// The LDAP distinguished name.
+ /// The value of the first RDN component, or the input unchanged if it contains no '=' separator.
internal static string ExtractFirstRdnValue(string distinguishedName)
{
int equalsIndex = distinguishedName.IndexOf('=');
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardLiveDataService.cs b/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardLiveDataService.cs
index 41f1d09..55e9c56 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardLiveDataService.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardLiveDataService.cs
@@ -193,7 +193,8 @@ public sealed class DashboardLiveDataService : IDashboardLiveDataService, IAsync
}
}
- ///
+ /// Closes the underlying gateway session, if one is open.
+ /// A task that represents the asynchronous operation.
public async ValueTask DisposeAsync()
{
if (_disposed)
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardRoles.cs b/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardRoles.cs
index 3caf61a..545d8ce 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardRoles.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardRoles.cs
@@ -8,7 +8,7 @@ public static class DashboardRoles
{
///
/// Read-write access: API-key CRUD, settings, any state-changing UI.
- /// Canonical role value (Task 1.7); formerly "Admin" — pure value
+ /// Canonical role value; formerly "Admin" — pure value
/// rename, the operations this role authorizes are unchanged.
///
public const string Admin = "Administrator";
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardServiceCollectionExtensions.cs b/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardServiceCollectionExtensions.cs
index 880f25e..a50b502 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardServiceCollectionExtensions.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardServiceCollectionExtensions.cs
@@ -25,6 +25,7 @@ public static class DashboardServiceCollectionExtensions
/// from the MxGateway:Ldap section. Also read to select the dashboard
/// authentication scheme via the MxGateway:Dashboard:DisableLogin dev flag.
///
+ /// The service collection, for chaining.
public static IServiceCollection AddGatewayDashboard(
this IServiceCollection services,
IConfiguration configuration)
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardSessionAdminResult.cs b/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardSessionAdminResult.cs
index 136e92a..710c53d 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardSessionAdminResult.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardSessionAdminResult.cs
@@ -6,6 +6,7 @@ public sealed record DashboardSessionAdminResult(
{
/// Creates a successful result with the given message.
/// The result message.
+ /// A successful .
public static DashboardSessionAdminResult Success(string message)
{
return new DashboardSessionAdminResult(true, message);
@@ -13,6 +14,7 @@ public sealed record DashboardSessionAdminResult(
/// Creates a failed result with the given message.
/// The result message.
+ /// A failed .
public static DashboardSessionAdminResult Fail(string message)
{
return new DashboardSessionAdminResult(false, message);
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardSessionAdminService.cs b/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardSessionAdminService.cs
index 8f3bb3d..b19b1a1 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardSessionAdminService.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardSessionAdminService.cs
@@ -93,10 +93,6 @@ public sealed class DashboardSessionAdminService(
}
catch (Exception exception)
{
- // Server-050: any non-SessionManagerException (e.g. an IOException or
- // InvalidOperationException from the session DisposeAsync / pipe teardown
- // path) used to propagate raw into Blazor's error boundary. Convert it to
- // a friendly failure so the Razor pages see only DashboardSessionAdminResult.
_logger.LogWarning(
exception,
"Dashboard admin {Actor} close failed unexpectedly for session {SessionId}.",
@@ -162,12 +158,6 @@ public sealed class DashboardSessionAdminService(
}
catch (Exception exception)
{
- // Server-050: any non-SessionManagerException (e.g. an IOException from
- // worker pipe teardown surfacing through session.DisposeAsync, or an
- // InvalidOperationException from a corrupted worker handle) used to
- // propagate raw into Blazor's error boundary. Convert it to a friendly
- // failure so the page renders the ResultMessage rather than the circuit
- // error page.
_logger.LogWarning(
exception,
"Dashboard admin {Actor} kill failed unexpectedly for session {SessionId}.",
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardSnapshotService.cs b/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardSnapshotService.cs
index 371bca2..9d2675b 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardSnapshotService.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardSnapshotService.cs
@@ -35,7 +35,7 @@ public sealed class DashboardSnapshotService : IDashboardSnapshotService
// sequence means the breakdown is unchanged and can be reused — keeping the ~1s snapshot tick
// O(1) for Galaxy. The summary's cheap volatile fields (status, timestamps, last error, counts)
// are NOT memoized: the library mutates them in place at the same sequence on steady-state ticks
- // and on refresh failure, so they are copied fresh on every snapshot (see Server-059).
+ // and on refresh failure, so they are copied fresh on every snapshot.
private GalaxyBreakdownCache? _galaxyBreakdownCache;
/// Initializes a new instance of the DashboardSnapshotService class.
@@ -72,10 +72,7 @@ public sealed class DashboardSnapshotService : IDashboardSnapshotService
_logger = logger ?? NullLogger.Instance;
}
- ///
- /// Gets a current dashboard snapshot of gateway state.
- ///
- /// Dashboard snapshot.
+ ///
public DashboardSnapshot GetSnapshot()
{
DateTimeOffset generatedAt = _timeProvider.GetUtcNow();
@@ -133,11 +130,7 @@ public sealed class DashboardSnapshotService : IDashboardSnapshotService
private sealed record GalaxyBreakdownCache(long Sequence, GalaxyObjectBreakdown Breakdown);
- ///
- /// Watches dashboard snapshots at regular intervals asynchronously.
- ///
- /// Cancellation token.
- /// Async enumerable of dashboard snapshots.
+ ///
public async IAsyncEnumerable WatchSnapshotsAsync(
[EnumeratorCancellation] CancellationToken cancellationToken)
{
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Dashboard/HubTokenService.cs b/src/ZB.MOM.WW.MxGateway.Server/Dashboard/HubTokenService.cs
index 52bd9af..773fb74 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Dashboard/HubTokenService.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Dashboard/HubTokenService.cs
@@ -12,7 +12,7 @@ namespace ZB.MOM.WW.MxGateway.Server.Dashboard;
/// protector; no separate signing keys are configured.
///
///
-/// Server-043: this service is registered as a singleton in
+/// This service is registered as a singleton in
/// and
/// is shared by two consumer scopes: DashboardHubConnectionFactory
/// (scoped, per-circuit; calls from the cookie-authenticated
@@ -40,6 +40,7 @@ public sealed class HubTokenService
/// Issues a bearer token carrying the user's identity and roles.
/// The claims principal representing the user.
+ /// The data-protected bearer token string.
public string Issue(ClaimsPrincipal user)
{
ArgumentNullException.ThrowIfNull(user);
@@ -52,6 +53,7 @@ public sealed class HubTokenService
/// Validates a token and returns the equivalent claims principal; null when invalid or expired.
/// The token string to validate.
+ /// The reconstructed , or when the token is missing, invalid, or expired.
public ClaimsPrincipal? Validate(string? token)
{
if (string.IsNullOrEmpty(token))
@@ -67,11 +69,6 @@ public sealed class HubTokenService
return null;
}
- // Server-039: reject a token whose payload carries no caller
- // identity. A null/empty Name AND NameIdentifier would otherwise
- // produce a principal that satisfies IsAuthenticated and IsInRole
- // checks without any associated user, because the AuthenticationType
- // (the HubToken scheme) is non-empty.
if (string.IsNullOrEmpty(payload.Name) && string.IsNullOrEmpty(payload.NameIdentifier))
{
return null;
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Dashboard/Hubs/DashboardEventBroadcaster.cs b/src/ZB.MOM.WW.MxGateway.Server/Dashboard/Hubs/DashboardEventBroadcaster.cs
index ae75937..ac44ec7 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Dashboard/Hubs/DashboardEventBroadcaster.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Dashboard/Hubs/DashboardEventBroadcaster.cs
@@ -14,9 +14,7 @@ public sealed class DashboardEventBroadcaster(
IHubContext hubContext,
ILogger logger) : IDashboardEventBroadcaster
{
- /// Publishes an MX event to connected dashboard clients.
- /// The session identifier.
- /// The MX event to publish.
+ ///
public void Publish(string sessionId, MxEvent mxEvent)
{
if (string.IsNullOrEmpty(sessionId) || mxEvent is null)
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Dashboard/Hubs/DashboardSnapshotPublisher.cs b/src/ZB.MOM.WW.MxGateway.Server/Dashboard/Hubs/DashboardSnapshotPublisher.cs
index 5d0a569..5c740fc 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Dashboard/Hubs/DashboardSnapshotPublisher.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Dashboard/Hubs/DashboardSnapshotPublisher.cs
@@ -9,7 +9,7 @@ namespace ZB.MOM.WW.MxGateway.Server.Dashboard.Hubs;
/// gateway process; clients listen via the hub.
///
///
-/// Server-042: wraps the snapshot subscription in
+/// wraps the snapshot subscription in
/// a reconnect loop with a configurable retry delay (5s by default,
/// mirroring ). A transient failure inside
/// — e.g. a
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Dashboard/Hubs/EventsHub.cs b/src/ZB.MOM.WW.MxGateway.Server/Dashboard/Hubs/EventsHub.cs
index 0560490..6d93a85 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Dashboard/Hubs/EventsHub.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Dashboard/Hubs/EventsHub.cs
@@ -27,7 +27,7 @@ public sealed class EventsHub : Hub
/// client.
///
///
- /// Server-038: in v1 the hub-level
+ /// In v1 the hub-level
/// (HubClientsPolicy) only checks that the caller carries one of
/// the dashboard roles (Admin or Viewer); both roles may subscribe to
/// any session id they choose. This is acceptable today because (a) the
@@ -43,6 +43,7 @@ public sealed class EventsHub : Hub
/// dedicated authorization policy applied to the hub method itself.
///
/// Session id to subscribe the caller to.
+ /// A task representing the subscription operation.
public Task SubscribeSession(string sessionId)
{
if (string.IsNullOrWhiteSpace(sessionId))
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Dashboard/IDashboardAuthenticator.cs b/src/ZB.MOM.WW.MxGateway.Server/Dashboard/IDashboardAuthenticator.cs
index 5496f4d..2f2adac 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Dashboard/IDashboardAuthenticator.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Dashboard/IDashboardAuthenticator.cs
@@ -11,6 +11,7 @@ public interface IDashboardAuthenticator
/// Username to authenticate.
/// Password to authenticate.
/// Token to cancel the asynchronous operation.
+ /// The authentication result.
Task AuthenticateAsync(
string? username,
string? password,
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Dashboard/IDashboardBrowseService.cs b/src/ZB.MOM.WW.MxGateway.Server/Dashboard/IDashboardBrowseService.cs
index ab857f1..e5096ea 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Dashboard/IDashboardBrowseService.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Dashboard/IDashboardBrowseService.cs
@@ -12,11 +12,13 @@ public interface IDashboardBrowseService
{
/// Returns root browse nodes (objects with no parent).
/// Filter arguments forwarded to the projector.
+ /// The root-level browse nodes and the cache sequence they were projected from.
BrowseLevelResult GetRoots(BrowseFilterArgs filter);
/// Returns the direct children of the given parent gobject id.
/// The Galaxy gobject id of the parent to expand.
/// Filter arguments forwarded to the projector.
+ /// The child browse nodes for the requested parent and the cache sequence they were projected from.
BrowseLevelResult GetChildren(int parentGobjectId, BrowseFilterArgs filter);
/// Current Galaxy cache sequence. Bumps after each successful refresh.
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Dashboard/IDashboardSnapshotService.cs b/src/ZB.MOM.WW.MxGateway.Server/Dashboard/IDashboardSnapshotService.cs
index ca33819..d72adfa 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Dashboard/IDashboardSnapshotService.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Dashboard/IDashboardSnapshotService.cs
@@ -8,11 +8,13 @@ public interface IDashboardSnapshotService
///
/// Gets the current dashboard snapshot.
///
+ /// The current dashboard snapshot.
DashboardSnapshot GetSnapshot();
///
/// Watches for changes to the dashboard state as an async enumerable.
///
/// Token to cancel the asynchronous operation.
+ /// An asynchronous stream of dashboard snapshots.
IAsyncEnumerable WatchSnapshotsAsync(CancellationToken cancellationToken);
}
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Diagnostics/AuthStoreHealthCheck.cs b/src/ZB.MOM.WW.MxGateway.Server/Diagnostics/AuthStoreHealthCheck.cs
index 30766a5..292fae2 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Diagnostics/AuthStoreHealthCheck.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Diagnostics/AuthStoreHealthCheck.cs
@@ -12,9 +12,15 @@ public sealed class AuthStoreHealthCheck : IHealthCheck
{
private readonly AuthSqliteConnectionFactory _connectionFactory;
+ /// Initializes a new instance of the class.
+ /// Factory used to open connections to the SQLite auth store.
public AuthStoreHealthCheck(AuthSqliteConnectionFactory connectionFactory) =>
_connectionFactory = connectionFactory ?? throw new ArgumentNullException(nameof(connectionFactory));
+ /// Verifies the SQLite auth store is reachable by executing a trivial query.
+ /// The health check context.
+ /// Token to cancel the asynchronous operation.
+ /// Healthy when the store responds; otherwise Unhealthy with the underlying exception.
public async Task CheckHealthAsync(
HealthCheckContext context,
CancellationToken cancellationToken = default)
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Diagnostics/GatewayLogRedactor.cs b/src/ZB.MOM.WW.MxGateway.Server/Diagnostics/GatewayLogRedactor.cs
index d04d2aa..b22b081 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Diagnostics/GatewayLogRedactor.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Diagnostics/GatewayLogRedactor.cs
@@ -19,6 +19,7 @@ public static class GatewayLogRedactor
/// Determines whether a command method bears credentials.
///
/// The command method name to check.
+ /// if the command method carries credentials; otherwise .
public static bool IsCredentialBearingCommand(string? commandMethod)
{
return commandMethod is not null
@@ -29,6 +30,7 @@ public static class GatewayLogRedactor
/// Redacts the API key secret portion of a Bearer authorization header.
///
/// The authorization header value to redact.
+ /// The header with the secret portion redacted, or the original value when it is null, blank, or not a Bearer header.
public static string? RedactApiKey(string? authorizationHeader)
{
if (string.IsNullOrWhiteSpace(authorizationHeader))
@@ -62,6 +64,7 @@ public static class GatewayLogRedactor
/// Redacts the client identity if it contains an API key.
///
/// The client identity string to redact.
+ /// The redacted client identity, or the original value when it contains no API key.
public static string? RedactClientIdentity(string? clientIdentity)
{
if (string.IsNullOrWhiteSpace(clientIdentity))
@@ -80,6 +83,7 @@ public static class GatewayLogRedactor
/// The command method name to check for credentials.
/// The command value to redact.
/// Whether value logging is enabled.
+ /// The redacted placeholder, the original value, or when is null.
public static object? RedactCommandValue(
string? commandMethod,
object? value,
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Diagnostics/GatewayLogScope.cs b/src/ZB.MOM.WW.MxGateway.Server/Diagnostics/GatewayLogScope.cs
index c80a2d4..0959cfa 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Diagnostics/GatewayLogScope.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Diagnostics/GatewayLogScope.cs
@@ -8,6 +8,7 @@ public sealed record GatewayLogScope(
string? ClientIdentity = null)
{
/// Converts the log scope to a dictionary with redacted sensitive fields.
+ /// A dictionary containing only the scope's present fields, with sensitive values redacted.
public IReadOnlyDictionary ToDictionary()
{
Dictionary values = [];
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Diagnostics/GatewayRequestLoggingMiddlewareExtensions.cs b/src/ZB.MOM.WW.MxGateway.Server/Diagnostics/GatewayRequestLoggingMiddlewareExtensions.cs
index 84a0af0..2b0e11b 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Diagnostics/GatewayRequestLoggingMiddlewareExtensions.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Diagnostics/GatewayRequestLoggingMiddlewareExtensions.cs
@@ -19,6 +19,7 @@ public static class GatewayRequestLoggingMiddlewareExtensions
/// Adds gateway request logging scope middleware that reads correlation headers and redacts sensitive data.
/// Application builder.
+ /// The same , for chaining.
public static IApplicationBuilder UseGatewayRequestLoggingScope(this IApplicationBuilder app)
{
ArgumentNullException.ThrowIfNull(app);
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Grpc/EventStreamService.cs b/src/ZB.MOM.WW.MxGateway.Server/Grpc/EventStreamService.cs
index d238ef4..19b1a4f 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Grpc/EventStreamService.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Grpc/EventStreamService.cs
@@ -13,13 +13,10 @@ public sealed class EventStreamService(
IOptions options,
GatewayMetrics metrics) : IEventStreamService
{
- ///
- /// Streams events from a session to the client asynchronously.
- ///
+ ///
///
///
- /// Task 4 rewired this from a per-RPC channel that drained the session directly
- /// to reading the subscriber's lease channel fed by the session's single
+ /// This reads the subscriber's lease channel fed by the session's single
/// pump. The pump owns the single drain of
/// the worker event stream and the worker→public mapping (mirroring the former
/// ProduceEventsAsync); this loop is the per-subscriber boundary that
@@ -27,33 +24,28 @@ public sealed class EventStreamService(
/// and the backpressure/overflow policy.
///
///
- /// Task 6 moved the dashboard mirror OFF this per-RPC loop. The dashboard is now a
+ /// The dashboard mirror runs OFF this per-RPC loop. The dashboard is a
/// first-class internal subscriber on the session's
/// (see GatewaySession.StartDashboardMirror),
- /// so it receives session events even when no gRPC client is streaming. This loop no
- /// longer mirrors to the dashboard. One deliberate consequence: the dashboard now sees
+ /// so it receives session events even when no gRPC client is streaming. This loop
+ /// does not mirror to the dashboard. One deliberate consequence: the dashboard sees
/// RAW session events, not the per-gRPC-subscriber AfterWorkerSequence-filtered
/// view this loop applies — the dashboard is a separate LDAP-authenticated monitoring
- /// view that should see the session's full event activity (per-session dashboard ACL is
- /// the separate Task 18).
+ /// view that should see the session's full event activity.
///
///
- /// Overflow handling (Task 5): the distributor's per-subscriber channel is bounded
+ /// Overflow handling: the distributor's per-subscriber channel is bounded
/// and the pump writes non-blocking. When this subscriber's channel is full the pump
/// applies the per-subscriber backpressure policy and completes this subscriber's
/// channel with a
/// (). That terminal fault
- /// surfaces here when the reader's MoveNextAsync throws, and — like the
- /// pre-epic per-RPC overflow — it propagates to the gRPC client unchanged. The
- /// overflow metric, and (in the legacy single-subscriber FailFast case) the session
- /// fault + fault metric, are recorded by the distributor's overflow handler so the
- /// session, the pump, and other subscribers are isolated from this subscriber's
- /// slowness.
+ /// surfaces here when the reader's MoveNextAsync throws, and it propagates to
+ /// the gRPC client unchanged. The overflow metric, and (in the legacy
+ /// single-subscriber FailFast case) the session fault + fault metric, are recorded by
+ /// the distributor's overflow handler so the session, the pump, and other subscribers
+ /// are isolated from this subscriber's slowness.
///
///
- /// Stream events request.
- /// Cancellation token.
- /// Async enumerable of MX events.
public async IAsyncEnumerable StreamEventsAsync(
StreamEventsRequest request,
[EnumeratorCancellation] CancellationToken cancellationToken)
@@ -72,7 +64,7 @@ public sealed class EventStreamService(
// the session's own SessionEventStreaming.AllowMultipleEventSubscribers field — the
// same source the distributor uses — so the two cannot diverge.
//
- // Reconnect/resume (Task 12): when AfterWorkerSequence > 0 the client is resuming, so
+ // Reconnect/resume: when AfterWorkerSequence > 0 the client is resuming, so
// attach via the replay variant that atomically snapshots the replay ring AND registers
// the live subscriber under one lock. That single critical section is the crux of the
// no-gap/no-duplicate handoff: every replayed event has sequence <= LiveResumeSequence
@@ -81,7 +73,7 @@ public sealed class EventStreamService(
// channel is dropped exactly once, while no newer event is skipped. See
// SessionEventDistributor.RegisterWithReplay for the full argument.
//
- // AfterWorkerSequence == 0 (fresh stream, not a resume) keeps the pre-Task-12 behavior:
+ // AfterWorkerSequence == 0 (fresh stream, not a resume) keeps the original behavior:
// a plain attach, no replay, no sentinel, and the live filter watermark stays 0.
ulong afterWorkerSequence = request.AfterWorkerSequence;
IEventSubscriberLease subscriber;
@@ -157,7 +149,7 @@ public sealed class EventStreamService(
{
// The distributor pump completes every subscriber channel with the source
// fault when the worker event stream terminates abnormally; that surfaces
- // here. Mirror the pre-Task-4 ProduceEventsAsync behavior: fault the
+ // here. Mirror the original ProduceEventsAsync behavior: fault the
// session and record the metric, then propagate the terminal fault to the
// gRPC client.
session.MarkFaulted(workerException.Message);
@@ -175,7 +167,7 @@ public sealed class EventStreamService(
// Queue-depth gauge tracks events the pump has fanned into this subscriber's
// channel but the client has not yet consumed — the same "buffered, not yet
- // delivered" quantity the pre-Task-4 per-RPC channel reported. The bounded
+ // delivered" quantity the original per-RPC channel reported. The bounded
// subscriber channel supports counting, so reconcile the gauge to the current
// backlog; falling back to a no-op delta if a channel ever cannot count.
int backlog = subscriber.Reader.CanCount ? subscriber.Reader.Count : streamQueueDepth;
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Grpc/IEventStreamService.cs b/src/ZB.MOM.WW.MxGateway.Server/Grpc/IEventStreamService.cs
index ace7fce..780e829 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Grpc/IEventStreamService.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Grpc/IEventStreamService.cs
@@ -12,6 +12,7 @@ public interface IEventStreamService
///
/// Request payload.
/// Token to cancel the asynchronous operation.
+ /// The events emitted for the requested session.
IAsyncEnumerable StreamEventsAsync(
StreamEventsRequest request,
CancellationToken cancellationToken);
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Grpc/MxAccessGatewayService.cs b/src/ZB.MOM.WW.MxGateway.Server/Grpc/MxAccessGatewayService.cs
index e88fbae..67a7ae8 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Grpc/MxAccessGatewayService.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Grpc/MxAccessGatewayService.cs
@@ -164,15 +164,6 @@ public sealed class MxAccessGatewayService(
}
///
- ///
- /// Surfaces the public AcknowledgeAlarm RPC. Acknowledgement is
- /// session-less: the gateway routes it through the always-on
- /// monitor session. An
- /// alarm_full_reference that parses as a canonical GUID forwards
- /// to AcknowledgeAlarmCommand; a Provider!Group.Tag
- /// reference forwards to AcknowledgeAlarmByNameCommand; anything
- /// else returns an InvalidRequest diagnostic in the reply.
- ///
public override async Task AcknowledgeAlarm(
AcknowledgeAlarmRequest request,
ServerCallContext context)
@@ -195,14 +186,6 @@ public sealed class MxAccessGatewayService(
}
///
- ///
- /// Surfaces the public StreamAlarms RPC — the session-less central
- /// alarm feed. The stream opens with one active_alarm per
- /// currently-active alarm, then a single snapshot_complete, then
- /// a transition for every subsequent change. Served by the
- /// gateway's always-on monitor; any
- /// number of clients fan out from the single monitor.
- ///
public override async Task StreamAlarms(
StreamAlarmsRequest request,
IServerStreamWriter responseStream,
@@ -226,12 +209,6 @@ public sealed class MxAccessGatewayService(
}
///
- ///
- /// Snapshot of the active-alarm cache maintained by the gateway's
- /// always-on alarm monitor. Streams one
- /// per currently-active alarm and completes — no transitions are
- /// emitted. Use StreamAlarms for a live transition feed.
- ///
public override async Task QueryActiveAlarms(
QueryActiveAlarmsRequest request,
IServerStreamWriter responseStream,
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Grpc/MxAccessGrpcMapper.cs b/src/ZB.MOM.WW.MxGateway.Server/Grpc/MxAccessGrpcMapper.cs
index 6e7e354..4333b8c 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Grpc/MxAccessGrpcMapper.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Grpc/MxAccessGrpcMapper.cs
@@ -23,6 +23,7 @@ public sealed class MxAccessGrpcMapper
/// Maps a gRPC MX command request to a worker command.
///
/// Request payload.
+ /// The mapped ready for worker dispatch.
public WorkerCommand MapCommand(MxCommandRequest request)
{
ArgumentNullException.ThrowIfNull(request);
@@ -39,6 +40,7 @@ public sealed class MxAccessGrpcMapper
/// Maps a worker command reply to a gRPC MX command reply.
///
/// Worker command reply.
+ /// The mapped , or a protocol-violation reply if the worker reply carried no public payload.
public MxCommandReply MapCommandReply(WorkerCommandReply reply)
{
ArgumentNullException.ThrowIfNull(reply);
@@ -58,6 +60,7 @@ public sealed class MxAccessGrpcMapper
/// Maps a worker event to a gRPC MX event.
///
/// Worker event to map.
+ /// The mapped , or an unspecified-family event if the worker event carried no public payload.
public MxEvent MapEvent(WorkerEvent workerEvent)
{
ArgumentNullException.ThrowIfNull(workerEvent);
@@ -73,6 +76,7 @@ public sealed class MxAccessGrpcMapper
/// Creates an OK protocol status.
///
/// Status message.
+ /// A with code .
public static ProtocolStatus Ok(string message = "OK")
{
return new ProtocolStatus
@@ -86,6 +90,7 @@ public sealed class MxAccessGrpcMapper
/// Creates an InvalidRequest protocol status.
///
/// Status message.
+ /// A with code .
public static ProtocolStatus InvalidRequest(string message)
{
return new ProtocolStatus
@@ -99,6 +104,7 @@ public sealed class MxAccessGrpcMapper
/// Creates a SessionNotFound protocol status.
///
/// Status message.
+ /// A with code .
public static ProtocolStatus SessionNotFound(string message)
{
return new ProtocolStatus
@@ -112,6 +118,7 @@ public sealed class MxAccessGrpcMapper
/// Creates a SessionNotReady protocol status.
///
/// Status message.
+ /// A with code .
public static ProtocolStatus SessionNotReady(string message)
{
return new ProtocolStatus
@@ -125,6 +132,7 @@ public sealed class MxAccessGrpcMapper
/// Creates a WorkerUnavailable protocol status.
///
/// Status message.
+ /// A with code .
public static ProtocolStatus WorkerUnavailable(string message)
{
return new ProtocolStatus
@@ -138,6 +146,7 @@ public sealed class MxAccessGrpcMapper
/// Creates a Timeout protocol status.
///
/// Status message.
+ /// A with code .
public static ProtocolStatus Timeout(string message)
{
return new ProtocolStatus
@@ -151,6 +160,7 @@ public sealed class MxAccessGrpcMapper
/// Creates a Canceled protocol status.
///
/// Status message.
+ /// A with code .
public static ProtocolStatus Canceled(string message)
{
return new ProtocolStatus
@@ -164,6 +174,7 @@ public sealed class MxAccessGrpcMapper
/// Creates a ProtocolViolation protocol status.
///
/// Status message.
+ /// A with code .
public static ProtocolStatus ProtocolViolation(string message)
{
return new ProtocolStatus
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Metrics/GatewayMetrics.cs b/src/ZB.MOM.WW.MxGateway.Server/Metrics/GatewayMetrics.cs
index 15fad05..2164661 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Metrics/GatewayMetrics.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Metrics/GatewayMetrics.cs
@@ -89,7 +89,7 @@ public sealed class GatewayMetrics : IDisposable
/// (via InternalsVisibleTo) so a can filter measurements by
/// against this specific instance rather than by the
/// process-shared , which would cross-talk between parallel tests that
- /// each build their own (Tests-027).
+ /// each build their own .
///
internal Meter Meter => _meter;
@@ -413,6 +413,7 @@ public sealed class GatewayMetrics : IDisposable
};
/// Sets the current alarm provider-mode gauge without recording a switch (e.g. startup baseline).
+ /// The alarm provider mode value to record.
public void SetAlarmProviderMode(int mode)
{
lock (_syncRoot) { _alarmProviderMode = mode; }
@@ -421,6 +422,7 @@ public sealed class GatewayMetrics : IDisposable
///
/// Returns a snapshot of all current metric values.
///
+ /// The current metrics snapshot.
public GatewayMetricsSnapshot GetSnapshot()
{
lock (_syncRoot)
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Security/Audit/CanonicalAuditWriter.cs b/src/ZB.MOM.WW.MxGateway.Server/Security/Audit/CanonicalAuditWriter.cs
index 06a7ad7..593849b 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Security/Audit/CanonicalAuditWriter.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Security/Audit/CanonicalAuditWriter.cs
@@ -19,7 +19,13 @@ public sealed class CanonicalAuditWriter(
SqliteCanonicalAuditStore store,
ILogger logger) : IAuditWriter
{
- ///
+ ///
+ /// Persists a canonical audit event to the underlying .
+ /// Any failure is caught, logged, and swallowed rather than propagated to the caller.
+ ///
+ /// The canonical audit event to persist.
+ /// Token to cancel the underlying store write.
+ /// A task that represents the asynchronous operation.
public async Task WriteAsync(AuditEvent auditEvent, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(auditEvent);
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Security/Audit/CanonicalForwardingApiKeyAuditStore.cs b/src/ZB.MOM.WW.MxGateway.Server/Security/Audit/CanonicalForwardingApiKeyAuditStore.cs
index befb60b..a24aa7b 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Security/Audit/CanonicalForwardingApiKeyAuditStore.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Security/Audit/CanonicalForwardingApiKeyAuditStore.cs
@@ -43,7 +43,10 @@ public sealed class CanonicalForwardingApiKeyAuditStore(
/// The library's keyless schema-init event type.
private const string InitDbEventType = "init-db";
- ///
+ /// Canonicalizes a library-emitted API-key audit entry onto and writes it via .
+ /// The library's API-key audit entry to forward.
+ /// Token to observe for cancellation.
+ /// A task that represents the asynchronous operation.
public async Task AppendAsync(ApiKeyAuditEntry entry, CancellationToken ct)
{
ArgumentNullException.ThrowIfNull(entry);
@@ -71,7 +74,10 @@ public sealed class CanonicalForwardingApiKeyAuditStore(
await auditWriter.WriteAsync(auditEvent, ct).ConfigureAwait(false);
}
- ///
+ /// Reads back the most recent entries from the canonical audit store and maps each one to an .
+ /// The maximum number of entries to return.
+ /// Token to observe for cancellation.
+ /// Up to recent API-key audit entries.
public async Task> ListRecentAsync(int limit, CancellationToken ct)
{
IReadOnlyList events = await store.ListRecentAsync(limit, ct).ConfigureAwait(false);
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Security/Audit/SqliteCanonicalAuditStore.cs b/src/ZB.MOM.WW.MxGateway.Server/Security/Audit/SqliteCanonicalAuditStore.cs
index 64c1133..a5ff0b8 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Security/Audit/SqliteCanonicalAuditStore.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Security/Audit/SqliteCanonicalAuditStore.cs
@@ -43,6 +43,7 @@ public sealed class SqliteCanonicalAuditStore(AuthSqliteConnectionFactory connec
/// Inserts a canonical audit event into the audit_event table.
/// The canonical event to persist.
/// Token to observe for cancellation.
+ /// A task that represents the asynchronous operation.
public async Task InsertAsync(AuditEvent auditEvent, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(auditEvent);
@@ -79,6 +80,7 @@ public sealed class SqliteCanonicalAuditStore(AuthSqliteConnectionFactory connec
/// Returns the most recent canonical audit events, newest first.
/// Maximum number of events to return.
/// Token to observe for cancellation.
+ /// The most recent audit events, up to , newest first.
public async Task> ListRecentAsync(int limit, CancellationToken cancellationToken)
{
if (limit <= 0)
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Security/Authentication/ApiKeyAdminCliRunner.cs b/src/ZB.MOM.WW.MxGateway.Server/Security/Authentication/ApiKeyAdminCliRunner.cs
index 24d01e6..643b14e 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Security/Authentication/ApiKeyAdminCliRunner.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Security/Authentication/ApiKeyAdminCliRunner.cs
@@ -26,6 +26,7 @@ public sealed class ApiKeyAdminCliRunner(ApiKeyAdminCommands commands)
/// API key administration command to execute.
/// Text writer for command output.
/// Token to cancel the asynchronous operation.
+ /// The process exit code for the executed command.
public async Task RunAsync(
ApiKeyAdminCommand command,
TextWriter output,
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Security/Authentication/ApiKeyAdminParseResult.cs b/src/ZB.MOM.WW.MxGateway.Server/Security/Authentication/ApiKeyAdminParseResult.cs
index 412e7b4..4f83ee1 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Security/Authentication/ApiKeyAdminParseResult.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Security/Authentication/ApiKeyAdminParseResult.cs
@@ -6,6 +6,7 @@ public sealed record ApiKeyAdminParseResult(
string? Error)
{
/// Returns a result indicating the input was not an API key command.
+ /// A result with set to .
public static ApiKeyAdminParseResult NotApiKeyCommand()
{
return new ApiKeyAdminParseResult(false, null, null);
@@ -13,6 +14,7 @@ public sealed record ApiKeyAdminParseResult(
/// Returns a successful parse result with the parsed API key command.
/// Parsed API key administration command.
+ /// A successful wrapping .
public static ApiKeyAdminParseResult Success(ApiKeyAdminCommand command)
{
return new ApiKeyAdminParseResult(true, command, null);
@@ -20,6 +22,7 @@ public sealed record ApiKeyAdminParseResult(
/// Returns a parse result with the specified error message.
/// Error message describing the parse failure.
+ /// A failed carrying .
public static ApiKeyAdminParseResult Fail(string error)
{
return new ApiKeyAdminParseResult(true, null, error);
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Security/Authentication/ApiKeyConstraintSerializer.cs b/src/ZB.MOM.WW.MxGateway.Server/Security/Authentication/ApiKeyConstraintSerializer.cs
index d2125ba..73b5575 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Security/Authentication/ApiKeyConstraintSerializer.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Security/Authentication/ApiKeyConstraintSerializer.cs
@@ -12,6 +12,7 @@ public static class ApiKeyConstraintSerializer
/// Serializes API key constraints to JSON, or returns null if the constraints are empty.
/// The constraints to serialize.
+ /// The serialized JSON, or when the constraints are empty.
public static string? Serialize(ApiKeyConstraints constraints)
{
ArgumentNullException.ThrowIfNull(constraints);
@@ -20,6 +21,7 @@ public static class ApiKeyConstraintSerializer
/// Deserializes API key constraints from JSON, or returns empty constraints if JSON is null or whitespace.
/// The JSON string to deserialize.
+ /// The deserialized constraints, or when is null/whitespace.
public static ApiKeyConstraints Deserialize(string? json)
{
if (string.IsNullOrWhiteSpace(json))
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Security/Authentication/AuthStoreServiceCollectionExtensions.cs b/src/ZB.MOM.WW.MxGateway.Server/Security/Authentication/AuthStoreServiceCollectionExtensions.cs
index 0554401..e9138b7 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Security/Authentication/AuthStoreServiceCollectionExtensions.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Security/Authentication/AuthStoreServiceCollectionExtensions.cs
@@ -66,10 +66,6 @@ public static class AuthStoreServiceCollectionExtensions
// migrator and the migration hosted service.
services.AddZbApiKeyAuth(effectiveConfig, AuthenticationSectionPath);
- // Canonical audit (Task 2.3 — DEEP adopt ZB.MOM.WW.Audit). All MxGateway audit flows as a
- // canonical AuditEvent through the library IAuditWriter, persisted in a NEW gateway-owned
- // audit_event table that lives in the SAME SQLite DB file as the api-key stores (it reuses
- // the library's AuthSqliteConnectionFactory, registered by AddZbApiKeyAuth above).
services.AddSingleton(sp =>
new SqliteCanonicalAuditStore(sp.GetRequiredService()));
// Resolve the logger defensively: the production host always registers ILogger, but the
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Security/Authorization/ConstraintEnforcer.cs b/src/ZB.MOM.WW.MxGateway.Server/Security/Authorization/ConstraintEnforcer.cs
index 6aebccc..e897c9e 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Security/Authorization/ConstraintEnforcer.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Security/Authorization/ConstraintEnforcer.cs
@@ -16,10 +16,7 @@ public sealed class ConstraintEnforcer(
IGalaxyHierarchyCache cache,
IAuditWriter auditWriter) : IConstraintEnforcer
{
- /// Checks read constraints on a tag address.
- /// The API key identity to check constraints for.
- /// Tag address to validate.
- /// Token to observe for cancellation.
+ ///
public Task CheckReadTagAsync(
ApiKeyIdentity? identity,
string tagAddress,
@@ -34,12 +31,7 @@ public sealed class ConstraintEnforcer(
return Task.FromResult(CheckReadTarget(constraints, tagAddress));
}
- /// Checks read constraints on a server and item handle.
- /// The API key identity to check constraints for.
- /// The gateway session containing handle registrations.
- /// The MXAccess server handle.
- /// The MXAccess item handle.
- /// Token to observe for cancellation.
+ ///
public Task CheckReadHandleAsync(
ApiKeyIdentity? identity,
GatewaySession session,
@@ -61,12 +53,7 @@ public sealed class ConstraintEnforcer(
return Task.FromResult(CheckReadTarget(constraints, registration.TagAddress));
}
- /// Checks write constraints on a server and item handle.
- /// The API key identity to check constraints for.
- /// The gateway session containing handle registrations.
- /// The MXAccess server handle.
- /// The MXAccess item handle.
- /// Token to observe for cancellation.
+ ///
public Task CheckWriteHandleAsync(
ApiKeyIdentity? identity,
GatewaySession session,
@@ -115,18 +102,7 @@ public sealed class ConstraintEnforcer(
return Task.FromResult(null);
}
- /// Records a constraint denial audit entry.
- /// The API key identity that was denied.
- /// The command type (e.g., read, write).
- /// The target being accessed (tag address or handle).
- /// The constraint failure details.
- ///
- /// The per-request client correlation id, if any. Persisted as the audit record's typed
- /// CorrelationId when it parses as a GUID; a non-GUID value leaves that column null.
- /// The raw string is always preserved in DetailsJson["clientCorrelationId"] so a
- /// non-GUID id (e.g. from Rust/Python/Java clients) is never silently lost.
- ///
- /// Token to observe for cancellation.
+ ///
public async Task RecordDenialAsync(
ApiKeyIdentity? identity,
string commandKind,
@@ -135,8 +111,8 @@ public sealed class ConstraintEnforcer(
string? correlationId,
CancellationToken cancellationToken)
{
- // Emit a canonical Denied AuditEvent directly through the best-effort IAuditWriter
- // (Task 2.3 #6): structured Target (":") and a richer DetailsJson
+ // Emit a canonical Denied AuditEvent directly through the best-effort IAuditWriter:
+ // structured Target (":") and a richer DetailsJson
// envelope carrying constraint/message/commandKind/target.
AuditEvent auditEvent = new()
{
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Security/Authorization/GatewayBrowseScopeProvider.cs b/src/ZB.MOM.WW.MxGateway.Server/Security/Authorization/GatewayBrowseScopeProvider.cs
index 88cec3e..a7426e0 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Security/Authorization/GatewayBrowseScopeProvider.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Security/Authorization/GatewayBrowseScopeProvider.cs
@@ -8,7 +8,9 @@ namespace ZB.MOM.WW.MxGateway.Server.Security.Authorization;
public sealed class GatewayBrowseScopeProvider(IGatewayRequestIdentityAccessor identityAccessor)
: IGalaxyBrowseScopeProvider
{
- ///
+ /// Resolves the Galaxy browse subtrees the calling API key is scoped to, from the ambient request identity.
+ /// The gRPC server call context for the in-flight browse request.
+ /// The caller's configured browse subtrees, or /empty when the caller is unscoped (full hierarchy).
public IReadOnlyList? ResolveBrowseSubtrees(ServerCallContext context)
{
// Invariant: the caller identity is the ambient one pushed by
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Security/Authorization/GatewayRequestIdentityAccessor.cs b/src/ZB.MOM.WW.MxGateway.Server/Security/Authorization/GatewayRequestIdentityAccessor.cs
index 8727deb..3c64516 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Security/Authorization/GatewayRequestIdentityAccessor.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Security/Authorization/GatewayRequestIdentityAccessor.cs
@@ -6,12 +6,10 @@ public sealed class GatewayRequestIdentityAccessor : IGatewayRequestIdentityAcce
{
private readonly AsyncLocal currentIdentity = new();
- /// Gets the current request identity.
+ ///
public ApiKeyIdentity? Current => currentIdentity.Value;
- /// Sets the current identity and returns a scope that restores the previous identity.
- /// The identity to push.
- /// Disposable scope.
+ ///
public IDisposable Push(ApiKeyIdentity identity)
{
ArgumentNullException.ThrowIfNull(identity);
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Security/Authorization/GrpcAuthorizationServiceCollectionExtensions.cs b/src/ZB.MOM.WW.MxGateway.Server/Security/Authorization/GrpcAuthorizationServiceCollectionExtensions.cs
index f174fc0..38ccd4d 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Security/Authorization/GrpcAuthorizationServiceCollectionExtensions.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Security/Authorization/GrpcAuthorizationServiceCollectionExtensions.cs
@@ -13,6 +13,7 @@ public static class GrpcAuthorizationServiceCollectionExtensions
/// Registers gRPC authorization middleware and scope resolver.
///
/// Service collection to register dependencies into.
+ /// The same service collection, for chaining.
public static IServiceCollection AddGatewayGrpcAuthorization(this IServiceCollection services)
{
services.AddSingleton();
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Security/Authorization/IConstraintEnforcer.cs b/src/ZB.MOM.WW.MxGateway.Server/Security/Authorization/IConstraintEnforcer.cs
index 12a7f0c..406f06e 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Security/Authorization/IConstraintEnforcer.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Security/Authorization/IConstraintEnforcer.cs
@@ -9,6 +9,7 @@ public interface IConstraintEnforcer
/// The API key identity.
/// Tag address to check.
/// Token to observe for cancellation.
+ /// The constraint failure details if denied; otherwise null.
Task CheckReadTagAsync(
ApiKeyIdentity? identity,
string tagAddress,
@@ -20,6 +21,7 @@ public interface IConstraintEnforcer
/// The MXAccess server handle.
/// The MXAccess item handle.
/// Token to observe for cancellation.
+ /// The constraint failure details if denied; otherwise null.
Task CheckReadHandleAsync(
ApiKeyIdentity? identity,
GatewaySession session,
@@ -33,6 +35,7 @@ public interface IConstraintEnforcer
/// The MXAccess server handle.
/// The MXAccess item handle.
/// Token to observe for cancellation.
+ /// The constraint failure details if denied; otherwise null.
Task CheckWriteHandleAsync(
ApiKeyIdentity? identity,
GatewaySession session,
@@ -50,6 +53,7 @@ public interface IConstraintEnforcer
/// CorrelationId when it parses as a GUID; otherwise left null.
///
/// Token to observe for cancellation.
+ /// A task that represents the asynchronous operation.
Task RecordDenialAsync(
ApiKeyIdentity? identity,
string commandKind,
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Security/Authorization/IGatewayRequestIdentityAccessor.cs b/src/ZB.MOM.WW.MxGateway.Server/Security/Authorization/IGatewayRequestIdentityAccessor.cs
index c6fe5f0..762a051 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Security/Authorization/IGatewayRequestIdentityAccessor.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Security/Authorization/IGatewayRequestIdentityAccessor.cs
@@ -10,5 +10,6 @@ public interface IGatewayRequestIdentityAccessor
/// Temporarily pushes an identity onto the scope stack, returning a handle to restore the previous state.
/// API key identity to push.
+ /// A disposable handle that restores the previous identity when disposed.
IDisposable Push(ApiKeyIdentity identity);
}
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Security/Tls/KestrelTlsInspector.cs b/src/ZB.MOM.WW.MxGateway.Server/Security/Tls/KestrelTlsInspector.cs
index 3c80573..5c3057c 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Security/Tls/KestrelTlsInspector.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Security/Tls/KestrelTlsInspector.cs
@@ -16,6 +16,8 @@ public static class KestrelTlsInspector
/// Certificate:Thumbprint), meaning the gateway must supply a
/// generated fallback certificate.
///
+ /// The application configuration to inspect for Kestrel certificate settings.
+ /// if the gateway must supply a generated fallback certificate; otherwise .
public static bool RequiresGeneratedCertificate(IConfiguration configuration)
{
// A Kestrel default certificate applies to every endpoint that lacks its own.
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Security/Tls/SelfSignedCertificateProvider.cs b/src/ZB.MOM.WW.MxGateway.Server/Security/Tls/SelfSignedCertificateProvider.cs
index 17b33cb..950fdfe 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Security/Tls/SelfSignedCertificateProvider.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Security/Tls/SelfSignedCertificateProvider.cs
@@ -20,6 +20,10 @@ public sealed class SelfSignedCertificateProvider
private readonly ILogger _logger;
private readonly TimeProvider _timeProvider;
+ /// Initializes a new instance of the class.
+ /// TLS options controlling certificate generation and persistence.
+ /// Logger for certificate generation and load diagnostics.
+ /// Time provider used for certificate validity and expiration checks.
public SelfSignedCertificateProvider(
TlsOptions options,
ILogger logger,
@@ -31,6 +35,7 @@ public sealed class SelfSignedCertificateProvider
}
/// Creates a fresh in-memory ECDSA P-256 self-signed certificate.
+ /// The generated self-signed certificate.
public X509Certificate2 GenerateCertificate()
{
using ECDsa key = ECDsa.Create(ECCurve.NamedCurves.nistP256);
@@ -89,6 +94,7 @@ public sealed class SelfSignedCertificateProvider
/// Loads the persisted certificate, regenerating when missing,
/// expired (and allowed), or unreadable.
+ /// The loaded or newly generated and persisted certificate.
public X509Certificate2 LoadOrCreate()
{
string path = _options.SelfSignedCertPath;
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Sessions/EventSubscriberReplayAttachment.cs b/src/ZB.MOM.WW.MxGateway.Server/Sessions/EventSubscriberReplayAttachment.cs
index 36a5c4a..45b9949 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Sessions/EventSubscriberReplayAttachment.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Sessions/EventSubscriberReplayAttachment.cs
@@ -4,7 +4,7 @@ namespace ZB.MOM.WW.MxGateway.Server.Sessions;
///
/// The result of a reconnect/resume attach
-/// (, Task 12): the live
+/// (): the live
/// subscriber lease plus the replay batch and resume watermarks snapshotted atomically
/// with the registration, so the replay→live handoff has no gap and no duplicate.
///
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Sessions/GatewaySession.cs b/src/ZB.MOM.WW.MxGateway.Server/Sessions/GatewaySession.cs
index 25d96a3..b5b9b55 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Sessions/GatewaySession.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Sessions/GatewaySession.cs
@@ -28,8 +28,7 @@ public sealed class GatewaySession
// True once at least one external subscriber attached SUCCESSFULLY. Detach-grace's
// "last subscriber dropped" stamp (see DetachEventSubscriber) is gated on this so a
// FAILED first attach — which still runs the rollback DetachEventSubscriber from the
- // attach catch path — does not push a never-subscribed session into the grace window
- // (Server-055).
+ // attach catch path — does not push a never-subscribed session into the grace window.
private bool _everHadEventSubscriber;
private SessionEventDistributor? _eventDistributor;
private bool _eventDistributorStarted;
@@ -115,7 +114,7 @@ public sealed class GatewaySession
///
///
/// Retention window kept after the last external (gRPC) event subscriber drops, so a
- /// client can reconnect (Task 12). When the window is positive and the active external
+ /// client can reconnect. When the window is positive and the active external
/// subscriber count falls to zero, the session stays
/// and records a detached timestamp; the lease monitor closes it once the window
/// elapses with no subscriber having re-attached. (the
@@ -389,7 +388,7 @@ public sealed class GatewaySession
/// session by walking it back to or any earlier
/// state. Both close-related writes (Closing and Closed) go through
/// _syncRoot just like every other state read/write, closing the split-lock
- /// race called out in Server-015.
+ /// race.
///
public void TransitionTo(SessionState nextState)
{
@@ -420,13 +419,13 @@ public sealed class GatewaySession
/// Transitions the session to the Ready state.
///
///
- /// On becoming Ready the session starts its internal dashboard mirror (Task 6) when a
+ /// On becoming Ready the session starts its internal dashboard mirror when a
/// dashboard broadcaster was supplied. The mirror registers an internal subscriber on
/// the distributor and starts the pump before any gRPC client attaches, so the
/// dashboard EventsHub receives session events even with no gRPC subscriber streaming —
/// fixing the "dark feed" where the dashboard only saw events while a gRPC client was
/// actively streaming. Registering the internal subscriber BEFORE
- /// also avoids the Task 4 hazard where
+ /// also avoids the hazard where
/// starting the pump at Ready with zero subscribers drained a fast-completing worker
/// stream into nothing and left a later subscriber hanging: there is now always a
/// subscriber (the dashboard one) registered before the pump starts.
@@ -444,7 +443,7 @@ public sealed class GatewaySession
// single drain to first-attach preserves that "events start flowing on subscribe"
// behavior and avoids draining a fast-completing source into the void before any
// subscriber exists. The source factory mirrors the mapping/ordering/start that
- // EventStreamService.ProduceEventsAsync used before Task 4: it drains the worker event
+ // EventStreamService.ProduceEventsAsync previously used: it drains the worker event
// stream in source order and maps each WorkerEvent to the public MxEvent with the same
// mapper, with no skip/filter — per-RPC filtering (e.g. AfterWorkerSequence) stays at the
// subscriber boundary in EventStreamService. Returns a registered lease atomically with
@@ -462,7 +461,7 @@ public sealed class GatewaySession
return lease;
}
- // Reconnect/resume variant of StartDistributorAndRegister (Task 12). Snapshots the replay
+ // Reconnect/resume variant of StartDistributorAndRegister. Snapshots the replay
// ring for events newer than afterSequence AND registers the live subscriber atomically
// under the distributor's replay lock, so the replay→live handoff has no gap and no
// duplicate (see SessionEventDistributor.RegisterWithReplay). The pump is started after
@@ -542,7 +541,7 @@ public sealed class GatewaySession
// once when the session becomes Ready (idempotent). The internal subscriber is registered
// BEFORE the pump starts (see StartDistributorAndRegister / EnsureDistributorCreated), so
// a subscriber is always present at pump start — the dashboard receives events with no
- // gRPC subscriber attached, and the Task 4 "zero-subscriber drain into the void" hang
+ // gRPC subscriber attached, and the "zero-subscriber drain into the void" hang
// cannot occur. No-op when no dashboard broadcaster was supplied (unit tests).
//
// Race-safety (Issue 1): _dashboardMirrorLease and _dashboardMirrorTask are published
@@ -605,14 +604,14 @@ public sealed class GatewaySession
}
// Reads the internal dashboard subscriber's channel and publishes each RAW fanned event
- // to the dashboard broadcaster. The dashboard is a first-class distributor subscriber
- // (Task 6), so it sees the session's full raw event activity — NOT the per-gRPC-subscriber
+ // to the dashboard broadcaster. The dashboard is a first-class distributor subscriber,
+ // so it sees the session's full raw event activity — NOT the per-gRPC-subscriber
// AfterWorkerSequence filtering that EventStreamService applies at its own boundary. This
// is intentional: the dashboard is a separate LDAP-authenticated monitoring view (per-
- // session dashboard ACL is the separate Task 18). Publish is best-effort / never-throw, so
+ // session dashboard ACL is a separate concern). Publish is best-effort / never-throw, so
// a slow or broken dashboard cannot fault the session or stall the pump; the bounded
- // internal subscriber channel (Task 5 per-subscriber isolation) only disconnects THIS
- // mirror on overflow, leaving the session and other subscribers untouched.
+ // internal subscriber channel only disconnects THIS mirror on overflow, leaving the
+ // session and other subscribers untouched.
private async Task RunDashboardMirrorAsync(
IDashboardEventBroadcaster broadcaster,
IEventSubscriberLease lease,
@@ -757,6 +756,7 @@ public sealed class GatewaySession
/// Determines whether the session lease has expired.
///
/// Current timestamp for comparison.
+ /// if the lease has expired with no active event subscriber; otherwise .
public bool IsLeaseExpired(DateTimeOffset now)
{
lock (_syncRoot)
@@ -778,6 +778,7 @@ public sealed class GatewaySession
/// window).
///
/// Current timestamp for comparison.
+ /// if the detach-grace window has elapsed with no re-attached subscriber; otherwise .
public bool IsDetachGraceExpired(DateTimeOffset now)
{
lock (_syncRoot)
@@ -813,6 +814,7 @@ public sealed class GatewaySession
/// succeed past it. On distributor-register failure the count is rolled back (see the
/// catch below).
///
+ /// A lease that reads the fanned public events for this subscriber.
public IEventSubscriberLease AttachEventSubscriber(int maxSubscribers)
{
// Derive the mode from the same source the distributor uses so the two can never
@@ -868,7 +870,7 @@ public sealed class GatewaySession
}
///
- /// Reconnect/resume variant of (Task 12). Attaches
+ /// Reconnect/resume variant of . Attaches
/// an event subscriber AND atomically snapshots the session replay ring for events newer
/// than , so a resuming client can replay what it missed
/// before live delivery resumes — with no gap and no duplicate across the handoff.
@@ -939,7 +941,7 @@ public sealed class GatewaySession
// Records that an external subscriber attached successfully. Gates the detach-grace
// "last subscriber dropped" stamp so a FAILED first attach (which still rolls back via
- // DetachEventSubscriber) never pushes a never-subscribed session into grace (Server-055).
+ // DetachEventSubscriber) never pushes a never-subscribed session into grace.
private void MarkEventSubscriberAttached()
{
lock (_syncRoot)
@@ -953,6 +955,7 @@ public sealed class GatewaySession
///
/// Worker command to invoke.
/// Token to cancel the asynchronous operation.
+ /// The worker's reply to the command.
public async Task InvokeAsync(
WorkerCommand command,
CancellationToken cancellationToken)
@@ -969,7 +972,7 @@ public sealed class GatewaySession
return await workerClient.InvokeAsync(command, CommandTimeout, cancellationToken).ConfigureAwait(false);
}
- // Single outbound choke point for the two array-write ergonomics shims (Task 3):
+ // Single outbound choke point for the two array-write ergonomics shims:
// 1. AddItem/AddItem2 array addresses gain the writable "[]" suffix when Galaxy metadata
// reports them as arrays, so the worker registers a write-capable handle. The mutation
// lands on the same MxCommand instance forwarded to the worker.
@@ -1063,6 +1066,7 @@ public sealed class GatewaySession
/// The MXAccess server handle.
/// The MXAccess item handle.
/// The item registration if found.
+ /// if a registration was found for the handle pair; otherwise .
public bool TryGetItemRegistration(
int serverHandle,
int itemHandle,
@@ -1136,6 +1140,7 @@ public sealed class GatewaySession
/// Server handle returned by the worker.
/// Tag addresses to add.
/// Token to cancel the asynchronous operation.
+ /// The per-address subscribe results.
public Task> AddItemBulkAsync(
int serverHandle,
IReadOnlyList tagAddresses,
@@ -1161,6 +1166,7 @@ public sealed class GatewaySession
/// Server handle returned by the worker.
/// Item handles to advise.
/// Token to cancel the asynchronous operation.
+ /// The per-handle subscribe results.
public Task> AdviseItemBulkAsync(
int serverHandle,
IReadOnlyList itemHandles,
@@ -1186,6 +1192,7 @@ public sealed class GatewaySession
/// Server handle returned by the worker.
/// Item handles to remove.
/// Token to cancel the asynchronous operation.
+ /// The per-handle subscribe results.
public Task> RemoveItemBulkAsync(
int serverHandle,
IReadOnlyList itemHandles,
@@ -1211,6 +1218,7 @@ public sealed class GatewaySession
/// Server handle returned by the worker.
/// Item handles to un-advise.
/// Token to cancel the asynchronous operation.
+ /// The per-handle subscribe results.
public Task> UnAdviseItemBulkAsync(
int serverHandle,
IReadOnlyList itemHandles,
@@ -1236,6 +1244,7 @@ public sealed class GatewaySession
/// Server handle returned by the worker.
/// Tag addresses to subscribe to.
/// Token to cancel the asynchronous operation.
+ /// The per-address subscribe results.
public Task> SubscribeBulkAsync(
int serverHandle,
IReadOnlyList tagAddresses,
@@ -1261,6 +1270,7 @@ public sealed class GatewaySession
/// Server handle returned by the worker.
/// Item handles to unsubscribe from.
/// Token to cancel the asynchronous operation.
+ /// The per-handle subscribe results.
public Task> UnsubscribeBulkAsync(
int serverHandle,
IReadOnlyList itemHandles,
@@ -1284,6 +1294,7 @@ public sealed class GatewaySession
/// Server handle returned by the worker.
/// Write entries to execute.
/// Token to cancel the asynchronous operation.
+ /// The per-entry write results.
public Task> WriteBulkAsync(
int serverHandle,
IReadOnlyList entries,
@@ -1307,6 +1318,7 @@ public sealed class GatewaySession
/// Server handle returned by the worker.
/// Write entries to execute.
/// Token to cancel the asynchronous operation.
+ /// The per-entry write results.
public Task> Write2BulkAsync(
int serverHandle,
IReadOnlyList entries,
@@ -1330,6 +1342,7 @@ public sealed class GatewaySession
/// Server handle returned by the worker.
/// Write entries to execute.
/// Token to cancel the asynchronous operation.
+ /// The per-entry write results.
public Task> WriteSecuredBulkAsync(
int serverHandle,
IReadOnlyList entries,
@@ -1353,6 +1366,7 @@ public sealed class GatewaySession
/// Server handle returned by the worker.
/// Write entries to execute.
/// Token to cancel the asynchronous operation.
+ /// The per-entry write results.
public Task> WriteSecured2BulkAsync(
int serverHandle,
IReadOnlyList entries,
@@ -1380,6 +1394,7 @@ public sealed class GatewaySession
/// Tag addresses to read.
/// Timeout for the read operation.
/// Token to cancel the asynchronous operation.
+ /// The per-address read results.
public Task> ReadBulkAsync(
int serverHandle,
IReadOnlyList tagAddresses,
@@ -1437,6 +1452,7 @@ public sealed class GatewaySession
/// / and a concurrent
/// TransitionTo(Ready) cannot race past a Closing write.
///
+ /// The outcome of the close operation.
public async Task CloseAsync(
string reason,
CancellationToken cancellationToken)
@@ -1613,7 +1629,7 @@ public sealed class GatewaySession
/// Mirrors 's use of _closeLock so that
/// a Close in flight from one caller and a Kill from another do not
/// race on the "was the session already closed" observation that
- /// drives metric increments (Server-045).
+ /// drives metric increments.
///
/// Reason for killing the worker.
/// Cancellation token.
@@ -1652,6 +1668,7 @@ public sealed class GatewaySession
/// The acquire is best-effort: a non-cancellable wait that swallows
/// so double-dispose still completes.
///
+ /// A task that represents the asynchronous operation.
public async ValueTask DisposeAsync()
{
try
@@ -1991,7 +2008,7 @@ public sealed class GatewaySession
// session instead of letting it linger only on the (long) lease: stamp the detached
// time so the lease monitor can close it once the grace window elapses. The session
// stays in its current (Ready) state and remains usable, so a reconnecting subscriber
- // (Task 12) re-attaches normally. The gateway-owned internal dashboard subscriber is
+ // re-attaches normally. The gateway-owned internal dashboard subscriber is
// NOT counted in _activeEventSubscriberCount (it registers on the distributor with
// isInternal: true), so a session whose only remaining subscriber is the dashboard
// mirror still enters grace. Only stamp while the session is alive — once
@@ -2001,7 +2018,7 @@ public sealed class GatewaySession
// Only stamp a detach that mirrors a prior SUCCESSFUL attach. The attach catch path
// calls this same method to roll back a reserved slot when the FIRST attach failed
// before any subscriber registered; that never-subscribed session must not enter the
- // grace window (Server-055).
+ // grace window.
if (_everHadEventSubscriber
&& _detachGrace > TimeSpan.Zero
&& _activeEventSubscriberCount == 0
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Sessions/ISessionManager.cs b/src/ZB.MOM.WW.MxGateway.Server/Sessions/ISessionManager.cs
index 3b146fd..f9c200e 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Sessions/ISessionManager.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Sessions/ISessionManager.cs
@@ -74,5 +74,6 @@ public interface ISessionManager
/// Shuts down all sessions and the session manager.
/// Token to cancel the asynchronous operation.
+ /// A task that represents the asynchronous operation.
Task ShutdownAsync(CancellationToken cancellationToken);
}
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Sessions/SessionEventDistributor.cs b/src/ZB.MOM.WW.MxGateway.Server/Sessions/SessionEventDistributor.cs
index c35bc63..8d94da3 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Sessions/SessionEventDistributor.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Sessions/SessionEventDistributor.cs
@@ -14,12 +14,12 @@ namespace ZB.MOM.WW.MxGateway.Server.Sessions;
///
///
/// when FailFast is allowed to fault the whole session for this
-/// overflow. As of Task 8 this is gated on the SESSION MODE, not a live count: it is
+/// overflow. This is gated on the SESSION MODE, not a live count: it is
/// only for an external subscriber in single-subscriber mode
/// (AllowMultipleEventSubscribers == false), where at most one external subscriber
/// can ever exist. In multi-subscriber mode it is always , so
/// FailFast degrades to a per-subscriber disconnect and one slow consumer never faults a
-/// session shared by others; gating on the fixed mode also removes the Task 5 race where a
+/// session shared by others; gating on the fixed mode also removes the race where a
/// concurrent registration could make a count snapshot falsely report a sole subscriber.
/// Always for internal subscribers (the dashboard mirror) so a
/// slow/broken dashboard can never fault the session.
@@ -38,15 +38,14 @@ public delegate void SubscriberOverflowHandler(bool isOnlySubscriber, bool isInt
///
///
///
-/// Introduced by Task 2 of the Session Resilience epic; the bounded replay ring
-/// buffer was added by Task 3, it was wired into GatewaySession and
-/// EventStreamService by Task 4, and the per-subscriber backpressure-isolation
-/// policy (Task 5) is implemented here: a slow subscriber overflows only its own
-/// bounded channel and the pump applies the policy to that subscriber alone (see
+/// The bounded replay ring buffer is wired into GatewaySession and
+/// EventStreamService. The per-subscriber backpressure-isolation policy is
+/// implemented here: a slow subscriber overflows only its own bounded channel and the
+/// pump applies the policy to that subscriber alone (see
/// and OnSubscriberOverflow), leaving
-/// the pump, the session, and other subscribers running. Task 8 made the
-/// FailFast-faults-session decision mode-gated: it fires only in single-subscriber
-/// mode (singleSubscriberMode), so multi-subscriber FailFast always degrades to
+/// the pump, the session, and other subscribers running. The FailFast-faults-session
+/// decision is mode-gated: it fires only in single-subscriber mode
+/// (singleSubscriberMode), so multi-subscriber FailFast always degrades to
/// a per-subscriber disconnect — see OnSubscriberOverflow. The ring buffer supports capacity
/// eviction (oldest entry dropped when the count exceeds
/// replayBufferCapacity) and age eviction (entries older than
@@ -58,7 +57,7 @@ public delegate void SubscriberOverflowHandler(bool isOnlySubscriber, bool isInt
/// producing an
/// of already-mapped public
/// s, given a . This is the
-/// cleanest seam for Task 4: it can pass
+/// cleanest seam: it can pass
/// ct => session.ReadEventsAsync(ct).Select(mapper.MapEvent) (or a
/// channel reader's ReadAllAsync), while unit tests pass a plain
/// channel reader's ReadAllAsync with no real session. The pump owns the
@@ -133,7 +132,7 @@ public sealed class SessionEventDistributor : IAsyncDisposable
/// Owning session id, used only for logging context.
///
/// Factory producing the session's event stream given a cancellation token.
- /// The pump consumes this exactly once. See the type remarks for the seam Task 4
+ /// The pump consumes this exactly once. See the type remarks for the seam this
/// plugs into.
///
///
@@ -141,10 +140,18 @@ public sealed class SessionEventDistributor : IAsyncDisposable
/// queue capacity shape used today.
///
/// Logger for pump lifecycle diagnostics.
+ ///
+ /// Optional per-subscriber backpressure handler invoked when a subscriber's bounded
+ /// channel is full. See the primary constructor overload for the full contract.
+ ///
+ ///
+ /// when the owning session is in single-subscriber mode. See
+ /// the primary constructor overload for the full contract.
+ ///
///
/// This overload disables the replay ring buffer (capacity 0). Use the overload
/// taking replay parameters to retain events for reconnect/reattach replay.
- /// Kept internal so production wiring (Task 4) cannot accidentally use
+ /// Kept internal so production wiring cannot accidentally use
/// the no-replay path; tests reach it via InternalsVisibleTo.
///
internal SessionEventDistributor(
@@ -173,7 +180,7 @@ public sealed class SessionEventDistributor : IAsyncDisposable
/// Owning session id, used only for logging context.
///
/// Factory producing the session's event stream given a cancellation token.
- /// The pump consumes this exactly once. See the type remarks for the seam Task 4
+ /// The pump consumes this exactly once. See the type remarks for the seam this
/// plugs into.
///
///
@@ -208,9 +215,9 @@ public sealed class SessionEventDistributor : IAsyncDisposable
/// overflows reports isOnlySubscriber == true (legacy FailFast faults the
/// session) ONLY in single-subscriber mode. In multi-subscriber mode it is always
/// , so FailFast degrades to a per-subscriber disconnect and a
- /// transient registration race can never falsely fault a shared session (Task 8;
- /// resolves the Task 5 REVISIT race). Defaults to so existing
- /// call sites and unit tests keep legacy single-subscriber FailFast behavior.
+ /// transient registration race can never falsely fault a shared session. Defaults to
+ /// so existing call sites and unit tests keep legacy
+ /// single-subscriber FailFast behavior.
///
public SessionEventDistributor(
string sessionId,
@@ -259,6 +266,7 @@ public sealed class SessionEventDistributor : IAsyncDisposable
/// Starts the background pump. Idempotent — a second call is a no-op.
///
/// Token observed only while starting.
+ /// A task that represents the asynchronous operation.
public Task StartAsync(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
@@ -285,7 +293,7 @@ public sealed class SessionEventDistributor : IAsyncDisposable
/// subscribers.
///
///
- /// for a gateway-owned internal subscriber (Task 6: the
+ /// for a gateway-owned internal subscriber (the
/// session's dashboard mirror) that must NOT participate in the single-subscriber
/// overflow accounting. An internal subscriber is excluded from the
/// isOnlySubscriber count, so a lone external gRPC subscriber still reports
@@ -296,6 +304,7 @@ public sealed class SessionEventDistributor : IAsyncDisposable
/// disconnected from the mirror. Defaults to (external
/// subscriber) so every existing call site is unchanged.
///
+ /// The lease for the newly-registered subscriber.
public IEventSubscriberLease Register(bool isInternal = false)
{
Channel channel = CreateSubscriberChannel();
@@ -355,7 +364,7 @@ public sealed class SessionEventDistributor : IAsyncDisposable
///
/// Atomically snapshots the replay ring for events newer than
/// AND registers a live subscriber, so the
- /// replay→live handoff has no gap and no duplicate (Task 12 reconnect/resume).
+ /// replay→live handoff has no gap and no duplicate (reconnect/resume).
///
///
/// The last worker sequence the reconnecting client already observed. Replay returns
@@ -389,6 +398,7 @@ public sealed class SessionEventDistributor : IAsyncDisposable
/// for a gateway-owned internal subscriber. See
/// .
///
+ /// The lease for the newly-registered subscriber.
///
///
/// Why this is atomic and the handoff is correct. The replay snapshot and the
@@ -488,6 +498,7 @@ public sealed class SessionEventDistributor : IAsyncDisposable
///
/// Stops the pump and completes all subscriber channels. Idempotent.
///
+ /// A task that represents the asynchronous operation.
public async ValueTask DisposeAsync()
{
Task? pumpTask;
@@ -603,22 +614,6 @@ public sealed class SessionEventDistributor : IAsyncDisposable
{
// Decide whether FailFast may fault the whole session for this overflow. This is the
// "isOnlySubscriber" signal the legacy single-subscriber FailFast path keys on.
- //
- // Task 8 resolution of the Task 5/7 REVISIT race: gate this on the SESSION MODE
- // (_singleSubscriberMode), NOT on a live count snapshot. The old
- // `CountExternalSubscribers() == 1` snapshot raced once multi-subscriber became real —
- // a concurrent second registration/unregistration could make the count read as 1 with
- // two subscribers actually present, producing a false FailFast that faults a shared
- // session. The mode is fixed for the session's lifetime, so reading it is race-free:
- // - single-subscriber mode: at most one external subscriber can ever exist (the
- // AttachEventSubscriber guard enforces it), so an overflowing external subscriber
- // IS the sole subscriber — preserve the legacy FailFast session-fault behavior.
- // - multi-subscriber mode: never fault the shared session; FailFast degrades to a
- // per-subscriber disconnect so one slow consumer cannot punish healthy ones.
- //
- // Task 6: the gateway-owned internal dashboard subscriber is excluded — an internal
- // subscriber that overflows is NEVER the "only subscriber", so a slow/broken dashboard
- // can only disconnect its own mirror and never fault the session.
bool isOnlySubscriber = !subscriber.IsInternal && _singleSubscriberMode;
_logger.LogDebug(
@@ -816,12 +811,17 @@ public sealed class SessionEventDistributor : IAsyncDisposable
private sealed class Subscriber(long id, Channel channel, bool isInternal)
{
+ /// Gets the subscriber's monotonic id, assigned at registration.
public long Id { get; } = id;
+ /// Gets the subscriber's own bounded event channel, written by the pump.
public Channel Channel { get; } = channel;
- // True for the gateway-owned internal dashboard subscriber. Excluded from the
- // single-subscriber overflow accounting so it cannot fault the session.
+ ///
+ /// Gets a value indicating whether this is the gateway-owned internal dashboard
+ /// subscriber. Excluded from the single-subscriber overflow accounting so it cannot
+ /// fault the session.
+ ///
public bool IsInternal { get; } = isInternal;
}
@@ -830,8 +830,13 @@ public sealed class SessionEventDistributor : IAsyncDisposable
{
private int _leaseDisposed;
+ ///
public ChannelReader Reader => subscriber.Channel.Reader;
+ ///
+ /// Unregisters the subscriber from the distributor and completes its channel.
+ /// Safe to call more than once; only the first call takes effect.
+ ///
public void Dispose()
{
// Atomic check-and-set so concurrent Dispose calls unregister at most once.
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Sessions/SessionEventStreaming.cs b/src/ZB.MOM.WW.MxGateway.Server/Sessions/SessionEventStreaming.cs
index 460bebf..2862a5f 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Sessions/SessionEventStreaming.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Sessions/SessionEventStreaming.cs
@@ -15,7 +15,7 @@ namespace ZB.MOM.WW.MxGateway.Server.Sessions;
///
/// Maps worker IPC WorkerEvent frames to public MxEvents. The distributor
/// pump applies this once per event in worker order, mirroring the mapping
-/// EventStreamService.ProduceEventsAsync used before Task 4.
+/// EventStreamService.ProduceEventsAsync used previously.
///
///
/// Supplies the distributor's per-subscriber queue capacity and replay ring-buffer
@@ -32,20 +32,20 @@ namespace ZB.MOM.WW.MxGateway.Server.Sessions;
/// preserving the observability the pre-epic per-RPC overflow path emitted.
///
///
-/// Sink the session's internal dashboard mirror loop (Task 6) publishes raw session
+/// Sink the session's internal dashboard mirror loop publishes raw session
/// MxEvents to. When non-null the session registers an internal distributor
/// subscriber on becoming Ready and mirrors every fanned event to the dashboard
/// EventsHub group regardless of whether a gRPC client is streaming. When null
/// (unit tests that don't exercise the dashboard mirror) no mirror is started.
///
///
-/// The session's effective multi-subscriber mode (Task 8). Carried here so the session
+/// The session's effective multi-subscriber mode. Carried here so the session
/// can pass it to its at construction — the
/// distributor is created at MarkReady (for the dashboard mirror) before any gRPC
/// subscriber attaches, so the mode cannot be learned from a later
/// AttachEventSubscriber call. The distributor gates its FailFast session-fault
/// decision on this mode (single-subscriber only) instead of a live count snapshot,
-/// closing the Task 5 false-FailFast race. Defaults to
+/// closing the false-FailFast race. Defaults to
/// (single-subscriber) so existing call sites and unit tests are unchanged.
///
public sealed record SessionEventStreaming(
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Sessions/SessionManager.cs b/src/ZB.MOM.WW.MxGateway.Server/Sessions/SessionManager.cs
index f9d6432..a848b99 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Sessions/SessionManager.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Sessions/SessionManager.cs
@@ -44,7 +44,7 @@ public sealed class SessionManager : ISessionManager
/// Logger passed to each session's event distributor pump.
///
/// Dashboard SignalR fan-out sink. Each session registers an internal distributor
- /// subscriber (Task 6) that mirrors raw session events to this broadcaster, so the
+ /// subscriber that mirrors raw session events to this broadcaster, so the
/// dashboard receives events regardless of whether a gRPC client is streaming. Null in
/// unit tests that do not exercise the dashboard mirror.
///
@@ -79,14 +79,7 @@ public sealed class SessionManager : ISessionManager
_sessionSlots = new SemaphoreSlim(_options.Sessions.MaxSessions, _options.Sessions.MaxSessions);
}
- ///
- /// Opens a new gateway session and connects to the worker.
- ///
- /// Session open request.
- /// Client authentication identity.
- /// API key identifier of the caller creating the session.
- /// Cancellation token.
- /// Opened gateway session.
+ ///
public async Task OpenSessionAsync(
SessionOpenRequest request,
string? clientIdentity,
@@ -151,12 +144,7 @@ public sealed class SessionManager : ISessionManager
}
}
- ///
- /// Attempts to retrieve a session by ID.
- ///
- /// Session identifier.
- /// The session if found.
- /// True if session found; otherwise false.
+ ///
public bool TryGetSession(
string sessionId,
[MaybeNullWhen(false)] out GatewaySession session)
@@ -164,13 +152,7 @@ public sealed class SessionManager : ISessionManager
return _registry.TryGet(sessionId, out session);
}
- ///
- /// Invokes a worker command on a session asynchronously.
- ///
- /// Session identifier.
- /// Worker command.
- /// Cancellation token.
- /// Command reply.
+ ///
public async Task InvokeAsync(
string sessionId,
WorkerCommand command,
@@ -197,12 +179,7 @@ public sealed class SessionManager : ISessionManager
}
}
- ///
- /// Reads events from a session's event stream asynchronously.
- ///
- /// Session identifier.
- /// Cancellation token.
- /// Async enumerable of worker events.
+ ///
public IAsyncEnumerable ReadEventsAsync(
string sessionId,
CancellationToken cancellationToken)
@@ -212,12 +189,7 @@ public sealed class SessionManager : ISessionManager
return session.ReadEventsAsync(cancellationToken);
}
- ///
- /// Closes a gateway session asynchronously.
- ///
- /// Session identifier.
- /// Cancellation token.
- /// Session close result.
+ ///
public async Task CloseSessionAsync(
string sessionId,
CancellationToken cancellationToken)
@@ -231,16 +203,12 @@ public sealed class SessionManager : ISessionManager
return result;
}
- ///
- /// Forcefully terminates a session's worker without attempting graceful shutdown.
+ ///
+ ///
/// Mirrors the registry/metrics cleanup that
/// performs after a successful close, but skips the WorkerClient.ShutdownAsync
/// step that would otherwise attempt.
- ///
- /// Session identifier.
- /// Reason recorded for the kill.
- /// Cancellation token.
- /// Session close result.
+ ///
public async Task KillWorkerAsync(
string sessionId,
string reason,
@@ -251,10 +219,6 @@ public sealed class SessionManager : ISessionManager
GatewaySession session = GetRequiredSession(sessionId);
- // Serialize concurrent kill/close attempts on this session by routing through the
- // per-session close lock (Server-045). Returns whether the session was already in
- // Closed state when the lock was acquired so the metric counter is incremented at
- // most once across concurrent callers.
bool wasClosed;
try
{
@@ -265,10 +229,6 @@ public sealed class SessionManager : ISessionManager
session.MarkFaulted(exception.Message);
_metrics.Fault(SessionManagerErrorCode.CloseFailed.ToString());
- // Server-044: the open-session gauge was incremented in OpenSessionAsync;
- // every session reaching KillWorkerAsync had SessionOpened recorded. If the
- // kill path throws, decrement the gauge here so mxgateway.sessions.open
- // does not leak — mirroring the Server-006 fix on OpenSessionAsync.
_metrics.SessionRemoved();
await RemoveSessionAsync(session).ConfigureAwait(false);
throw new SessionManagerException(
@@ -291,12 +251,7 @@ public sealed class SessionManager : ISessionManager
return new SessionCloseResult(sessionId, SessionState.Closed, AlreadyClosed: wasClosed);
}
- ///
- /// Closes all sessions with expired leases asynchronously.
- ///
- /// Current time for lease expiration check.
- /// Cancellation token.
- /// Count of sessions closed.
+ ///
public async Task CloseExpiredLeasesAsync(
DateTimeOffset now,
CancellationToken cancellationToken)
@@ -341,11 +296,7 @@ public sealed class SessionManager : ISessionManager
return closedCount;
}
- ///
- /// Shuts down all active sessions gracefully asynchronously.
- ///
- /// Cancellation token.
- /// Completed task.
+ ///
public async Task ShutdownAsync(CancellationToken cancellationToken)
{
foreach (GatewaySession session in _registry.Snapshot())
@@ -361,10 +312,6 @@ public sealed class SessionManager : ISessionManager
"Graceful shutdown failed for session {SessionId}; killing worker.",
session.SessionId);
- // Defensive fallback: CloseSessionCoreAsync's inner SessionCloseStartedException
- // catch normally removes the session and accounts the close (Server-046). The
- // outer fallback only fires for sessions still in the registry — route through
- // KillWorkerAsync so the bookkeeping is identical to the dashboard kill path.
if (_registry.TryGet(session.SessionId, out _))
{
try
@@ -409,11 +356,6 @@ public sealed class SessionManager : ISessionManager
session.MarkFaulted(exception.Message);
if (!wasClosed)
{
- // Server-046: account the close as a SessionClosed (decrements the open-session
- // gauge AND increments the sessions.closed counter), not just SessionRemoved.
- // The session is being removed from the registry below; treating this as a
- // half-finished close that only decremented the gauge under-counted the closed
- // counter.
_metrics.SessionClosed();
}
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Sessions/SessionOpenRequest.cs b/src/ZB.MOM.WW.MxGateway.Server/Sessions/SessionOpenRequest.cs
index c48eacf..3f6282c 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Sessions/SessionOpenRequest.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Sessions/SessionOpenRequest.cs
@@ -11,6 +11,7 @@ public sealed record SessionOpenRequest(
{
/// Creates a SessionOpenRequest from a gRPC OpenSessionRequest contract.
/// Request payload.
+ /// The equivalent .
public static SessionOpenRequest FromContract(OpenSessionRequest request)
{
ArgumentNullException.ThrowIfNull(request);
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Sessions/SessionRegistry.cs b/src/ZB.MOM.WW.MxGateway.Server/Sessions/SessionRegistry.cs
index ce145cc..e687a06 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Sessions/SessionRegistry.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Sessions/SessionRegistry.cs
@@ -11,20 +11,13 @@ public sealed class SessionRegistry : ISessionRegistry
{
private readonly ConcurrentDictionary _sessions = new(StringComparer.Ordinal);
- ///
- /// Gets the total count of sessions in the registry.
- ///
+ ///
public int Count => _sessions.Count;
- ///
- /// Gets the count of non-closed sessions.
- ///
+ ///
public int ActiveCount => _sessions.Values.Count(session => session.State is not SessionState.Closed);
- ///
- /// Adds a session to the registry.
- ///
- /// Gateway session to add.
+ ///
public bool TryAdd(GatewaySession session)
{
ArgumentNullException.ThrowIfNull(session);
@@ -32,11 +25,7 @@ public sealed class SessionRegistry : ISessionRegistry
return _sessions.TryAdd(session.SessionId, session);
}
- ///
- /// Retrieves a session by identifier.
- ///
- /// Identifier of the session.
- /// The retrieved session if found.
+ ///
public bool TryGet(
string sessionId,
[MaybeNullWhen(false)] out GatewaySession session)
@@ -44,11 +33,7 @@ public sealed class SessionRegistry : ISessionRegistry
return _sessions.TryGetValue(sessionId, out session);
}
- ///
- /// Removes a session from the registry by identifier.
- ///
- /// Identifier of the session.
- /// The removed session if found.
+ ///
public bool TryRemove(
string sessionId,
[MaybeNullWhen(false)] out GatewaySession session)
@@ -56,9 +41,7 @@ public sealed class SessionRegistry : ISessionRegistry
return _sessions.TryRemove(sessionId, out session);
}
- ///
- /// Returns a snapshot of all sessions in the registry.
- ///
+ ///
public IReadOnlyCollection Snapshot()
{
return _sessions.Values.ToArray();
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Sessions/SessionShutdownHostedService.cs b/src/ZB.MOM.WW.MxGateway.Server/Sessions/SessionShutdownHostedService.cs
index 426fa29..d126dfc 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Sessions/SessionShutdownHostedService.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Sessions/SessionShutdownHostedService.cs
@@ -8,13 +8,17 @@ public sealed class SessionShutdownHostedService(
ISessionManager sessionManager,
ILogger logger) : IHostedService
{
- ///
+ /// No-op startup hook; this service only has work to do on shutdown.
+ /// Token that signals startup should be aborted.
+ /// A task that represents the asynchronous operation.
public Task StartAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
- ///
+ /// Shuts down all gateway sessions as the host stops, logging (without throwing) if the host's shutdown timeout cancels the operation first.
+ /// Token that signals the host's shutdown timeout has elapsed.
+ /// A task that represents the asynchronous operation.
public async Task StopAsync(CancellationToken cancellationToken)
{
try
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Sessions/SessionWorkerClientFactory.cs b/src/ZB.MOM.WW.MxGateway.Server/Sessions/SessionWorkerClientFactory.cs
index eb95139..4a14f6b 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Sessions/SessionWorkerClientFactory.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Sessions/SessionWorkerClientFactory.cs
@@ -39,10 +39,7 @@ public sealed class SessionWorkerClientFactory : ISessionWorkerClientFactory
_options = options.Value;
}
- /// Creates a worker client and launches the worker process.
- /// The gateway session.
- /// Cancellation token.
- /// The created worker client.
+ ///
public async Task CreateAsync(
GatewaySession session,
CancellationToken cancellationToken)
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Workers/IWorkerClient.cs b/src/ZB.MOM.WW.MxGateway.Server/Workers/IWorkerClient.cs
index abad654..38bada2 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Workers/IWorkerClient.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Workers/IWorkerClient.cs
@@ -19,12 +19,14 @@ public interface IWorkerClient : IAsyncDisposable
/// Initiates the handshake and enters ready state.
/// Token to cancel the asynchronous operation.
+ /// A task that represents the asynchronous operation.
Task StartAsync(CancellationToken cancellationToken);
/// Sends a command to the worker and waits for a reply.
/// Worker command to invoke.
/// Timeout for waiting for the reply.
/// Token to cancel the asynchronous operation.
+ /// The worker's reply to the command.
Task InvokeAsync(
WorkerCommand command,
TimeSpan timeout,
@@ -32,11 +34,13 @@ public interface IWorkerClient : IAsyncDisposable
/// Reads events from the worker as they arrive.
/// Token to cancel the asynchronous operation.
+ /// An asynchronous stream of worker events.
IAsyncEnumerable ReadEventsAsync(CancellationToken cancellationToken);
/// Gracefully shuts down the worker by closing the connection.
/// Timeout for shutdown.
/// Token to cancel the asynchronous operation.
+ /// A task that represents the asynchronous operation.
Task ShutdownAsync(TimeSpan timeout, CancellationToken cancellationToken);
/// Terminates the worker process immediately with a diagnostic reason.
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Workers/IWorkerProcess.cs b/src/ZB.MOM.WW.MxGateway.Server/Workers/IWorkerProcess.cs
index 0249509..b36bcbd 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Workers/IWorkerProcess.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Workers/IWorkerProcess.cs
@@ -24,6 +24,7 @@ public interface IWorkerProcess : IDisposable
/// Waits for the process to exit with the specified cancellation token.
///
/// Token to cancel the asynchronous operation.
+ /// A task that represents the asynchronous operation.
ValueTask WaitForExitAsync(CancellationToken cancellationToken);
///
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Workers/OrphanWorkerCleanupHostedService.cs b/src/ZB.MOM.WW.MxGateway.Server/Workers/OrphanWorkerCleanupHostedService.cs
index efa4a49..bec1f45 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Workers/OrphanWorkerCleanupHostedService.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Workers/OrphanWorkerCleanupHostedService.cs
@@ -8,7 +8,9 @@ public sealed class OrphanWorkerCleanupHostedService(
OrphanWorkerTerminator terminator,
ILogger logger) : IHostedService
{
- ///
+ /// Terminates leftover orphan MXAccess worker processes once, best-effort, before the gateway starts accepting sessions.
+ /// Token to cancel the asynchronous operation.
+ /// A task that represents the asynchronous operation.
public Task StartAsync(CancellationToken cancellationToken)
{
try
@@ -25,6 +27,8 @@ public sealed class OrphanWorkerCleanupHostedService(
return Task.CompletedTask;
}
- ///
+ /// No-op; this service has no ongoing work to stop.
+ /// Token to cancel the asynchronous operation.
+ /// A task that represents the asynchronous operation.
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Workers/SystemWorkerProcess.cs b/src/ZB.MOM.WW.MxGateway.Server/Workers/SystemWorkerProcess.cs
index ac86232..69a5621 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Workers/SystemWorkerProcess.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Workers/SystemWorkerProcess.cs
@@ -28,7 +28,7 @@ internal sealed class SystemWorkerProcess(Process process) : IWorkerProcess
process.Kill(entireProcessTree);
}
- ///
+ /// Disposes the wrapped instance.
public void Dispose()
{
process.Dispose();
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Workers/WorkerClient.cs b/src/ZB.MOM.WW.MxGateway.Server/Workers/WorkerClient.cs
index 3761ddb..c2d3947 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Workers/WorkerClient.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Workers/WorkerClient.cs
@@ -78,10 +78,10 @@ public sealed class WorkerClient : IWorkerClient
_lastHeartbeatAt = _timeProvider.GetUtcNow();
}
- /// Gets the worker's session ID.
+ ///
public string SessionId => _connection.SessionId;
- /// Gets the worker process ID.
+ ///
public int? ProcessId
{
get
@@ -93,7 +93,7 @@ public sealed class WorkerClient : IWorkerClient
}
}
- /// Gets the current client state.
+ ///
public WorkerClientState State
{
get
@@ -105,7 +105,7 @@ public sealed class WorkerClient : IWorkerClient
}
}
- /// Gets the timestamp of the last received heartbeat.
+ ///
public DateTimeOffset LastHeartbeatAt
{
get
@@ -117,8 +117,7 @@ public sealed class WorkerClient : IWorkerClient
}
}
- /// Starts the worker client and completes the handshake.
- /// Cancellation token.
+ ///
public async Task StartAsync(CancellationToken cancellationToken)
{
ThrowIfDisposed();
@@ -141,11 +140,7 @@ public sealed class WorkerClient : IWorkerClient
_heartbeatLoopTask = Task.Run(HeartbeatLoopAsync);
}
- /// Invokes a command on the worker and waits for reply.
- /// The command to invoke.
- /// Command timeout.
- /// Cancellation token.
- /// The command reply.
+ ///
public async Task InvokeAsync(
WorkerCommand command,
TimeSpan timeout,
@@ -228,9 +223,7 @@ public sealed class WorkerClient : IWorkerClient
}
}
- /// Reads events from the worker as an async stream.
- /// Cancellation token.
- /// Async enumerable of worker events.
+ ///
public async IAsyncEnumerable ReadEventsAsync(
[EnumeratorCancellation] CancellationToken cancellationToken)
{
@@ -242,9 +235,7 @@ public sealed class WorkerClient : IWorkerClient
}
}
- /// Shuts down the worker gracefully.
- /// Shutdown timeout.
- /// Cancellation token.
+ ///
public async Task ShutdownAsync(TimeSpan timeout, CancellationToken cancellationToken)
{
ThrowIfDisposed();
@@ -289,8 +280,7 @@ public sealed class WorkerClient : IWorkerClient
}
}
- /// Terminates the worker process immediately.
- /// Reason for termination.
+ ///
public void Kill(string reason)
{
ThrowIfDisposed();
@@ -302,6 +292,7 @@ public sealed class WorkerClient : IWorkerClient
}
/// Disposes the worker client and releases resources.
+ /// A task that represents the asynchronous operation.
public async ValueTask DisposeAsync()
{
if (_disposed)
@@ -390,13 +381,13 @@ public sealed class WorkerClient : IWorkerClient
}
///
- /// Monitors worker heartbeat and detects stale sessions. Mirrors
- /// Worker-023 on the worker side: while a command is in flight on the
+ /// Monitors worker heartbeat and detects stale sessions. Mirrors the
+ /// worker-side watchdog: while a command is in flight on the
/// gateway↔worker pipe, the heartbeat watchdog is suppressed up to
/// — the worker
/// may be busy executing a slow STA command and the heartbeat write may
- /// be queued behind a long event-drain burst (Server-031), neither of
- /// which indicates the worker is actually hung. Once the oldest pending
+ /// be queued behind a long event-drain burst, neither of which
+ /// indicates the worker is actually hung. Once the oldest pending
/// command exceeds the ceiling, the fault fires anyway so a truly stuck
/// COM call doesn't hide the worker forever.
///
@@ -419,11 +410,6 @@ public sealed class WorkerClient : IWorkerClient
continue;
}
- // Server-031: if a command is in flight and hasn't yet exceeded
- // the stuck-command ceiling, the gap is more likely caused by
- // pipe-write contention (event drain holding the writer lock)
- // or a legitimately slow STA command than by a hung worker.
- // Wait for the ceiling before faulting on heartbeat alone.
if (TryGetOldestPendingCommandAge(out TimeSpan oldestCommandAge)
&& oldestCommandAge <= _options.HeartbeatStuckCeiling)
{
@@ -446,7 +432,7 @@ public sealed class WorkerClient : IWorkerClient
/// Returns the age of the oldest pending command on the worker pipe,
/// measured via against
/// , or false when no
- /// commands are pending. Used by the heartbeat watchdog (Server-031)
+ /// commands are pending. Used by the heartbeat watchdog
/// to decide whether a heartbeat gap is plausibly the result of
/// pipe-write contention rather than a hung worker.
///
@@ -508,12 +494,12 @@ public sealed class WorkerClient : IWorkerClient
}
///
- /// Enqueues a worker event for client consumption. Server-032: the
- /// channel is configured with
- /// and a brief consumer hiccup is now tolerated for up to
+ /// Enqueues a worker event for client consumption. The channel is
+ /// configured with
+ /// and a brief consumer hiccup is tolerated for up to
///
- /// (default 5s) before the worker is faulted. Pre-Server-032 the code
- /// used TryWrite (non-blocking) which never honored the
+ /// (default 5s) before the worker is faulted. The channel previously
+ /// used TryWrite (non-blocking), which never honored the
/// configured FullModeTimeout — the worker faulted on the first
/// missed slot even though the wait-mode channel would have absorbed
/// the burst. The diagnostic now names the capacity, current depth, and
@@ -538,8 +524,6 @@ public sealed class WorkerClient : IWorkerClient
return;
}
- // Channel is full; honor the configured wait timeout before declaring
- // the consumer dead and faulting the worker (Server-032).
using CancellationTokenSource fullModeCts = CancellationTokenSource
.CreateLinkedTokenSource(cancellationToken);
fullModeCts.CancelAfter(_options.EventChannelFullModeTimeout);
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Workers/WorkerClientOptions.cs b/src/ZB.MOM.WW.MxGateway.Server/Workers/WorkerClientOptions.cs
index 7c66651..6113cb9 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Workers/WorkerClientOptions.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Workers/WorkerClientOptions.cs
@@ -15,7 +15,7 @@ public sealed class WorkerClientOptions
///
/// Default ceiling on the in-flight-command heartbeat skip. Mirrors
///
- /// on the worker side (Worker-023). When a command has been in flight
+ /// on the worker side. When a command has been in flight
/// longer than this, the gateway-side heartbeat watchdog fires
/// regardless of pending commands — a truly stuck COM call shouldn't
/// hide the worker forever.
@@ -47,8 +47,7 @@ public sealed class WorkerClientOptions
/// faulting the worker. Honored by EnqueueWorkerEventAsync via
/// WriteAsync; with the channel configured for
/// BoundedChannelFullMode.Wait, a transient backlog only faults
- /// after the configured timeout has elapsed (Server-032). Pre-Server-032
- /// the field was declared but unused — overflow faulted immediately.
+ /// after the configured timeout has elapsed.
///
public TimeSpan EventChannelFullModeTimeout { get; init; }
@@ -56,12 +55,12 @@ public sealed class WorkerClientOptions
public int MaxPendingCommands { get; init; }
///
- /// Server-031: ceiling on the in-flight-command heartbeat-skip. When
+ /// Ceiling on the in-flight-command heartbeat-skip. When
/// a command has been pending on the gateway↔worker pipe for longer
/// than this, the gateway-side HeartbeatLoopAsync fires the
/// HeartbeatExpired fault even if commands are still pending;
/// a truly stuck COM call shouldn't keep the watchdog suppressed
- /// indefinitely. Mirrors Worker-023's HeartbeatStuckCeiling on
+ /// indefinitely. Mirrors HeartbeatStuckCeiling on
/// the worker side.
///
public TimeSpan HeartbeatStuckCeiling { get; init; }
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Workers/WorkerFrameWriter.cs b/src/ZB.MOM.WW.MxGateway.Server/Workers/WorkerFrameWriter.cs
index 999fab5..728a611 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Workers/WorkerFrameWriter.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Workers/WorkerFrameWriter.cs
@@ -30,6 +30,7 @@ public sealed class WorkerFrameWriter
///
/// Worker envelope message to write.
/// Token to cancel the asynchronous operation.
+ /// A task that represents the asynchronous operation.
public async ValueTask WriteAsync(
WorkerEnvelope envelope,
CancellationToken cancellationToken = default)
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Workers/WorkerProcessHandle.cs b/src/ZB.MOM.WW.MxGateway.Server/Workers/WorkerProcessHandle.cs
index 0ffb814..fe4f0b0 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Workers/WorkerProcessHandle.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Workers/WorkerProcessHandle.cs
@@ -30,7 +30,7 @@ public sealed class WorkerProcessHandle : IDisposable
/// Gets the time when the process was launched.
public DateTimeOffset LaunchedAt { get; }
- ///
+ /// Disposes the underlying worker process.
public void Dispose()
{
Process.Dispose();
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Workers/WorkerProcessLauncher.cs b/src/ZB.MOM.WW.MxGateway.Server/Workers/WorkerProcessLauncher.cs
index c8a6ab7..cab6fe1 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Workers/WorkerProcessLauncher.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Workers/WorkerProcessLauncher.cs
@@ -58,12 +58,7 @@ public sealed class WorkerProcessLauncher : IWorkerProcessLauncher
_logger = logger ?? NullLogger.Instance;
}
- ///
- /// Launches a worker process and waits for startup.
- ///
- /// Request payload.
- /// Token to cancel the asynchronous operation.
- /// Handle to the launched worker process.
+ ///
public async Task LaunchAsync(
WorkerProcessLaunchRequest request,
CancellationToken cancellationToken = default)
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Workers/WorkerProcessStartedProbe.cs b/src/ZB.MOM.WW.MxGateway.Server/Workers/WorkerProcessStartedProbe.cs
index 1e19a98..b9caedd 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Workers/WorkerProcessStartedProbe.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Workers/WorkerProcessStartedProbe.cs
@@ -2,11 +2,7 @@ namespace ZB.MOM.WW.MxGateway.Server.Workers;
public sealed class WorkerProcessStartedProbe : IWorkerStartupProbe
{
- /// Verifies that the worker process has started and has not exited.
- /// Worker process to verify.
- /// Process launch request.
- /// Token to cancel the asynchronous operation.
- /// Completed task if process is running.
+ ///
public Task WaitUntilReadyAsync(
IWorkerProcess process,
WorkerProcessLaunchRequest request,
diff --git a/src/ZB.MOM.WW.MxGateway.Server/Workers/WorkerServiceCollectionExtensions.cs b/src/ZB.MOM.WW.MxGateway.Server/Workers/WorkerServiceCollectionExtensions.cs
index d15e914..d52b5ae 100644
--- a/src/ZB.MOM.WW.MxGateway.Server/Workers/WorkerServiceCollectionExtensions.cs
+++ b/src/ZB.MOM.WW.MxGateway.Server/Workers/WorkerServiceCollectionExtensions.cs
@@ -5,6 +5,7 @@ public static class WorkerServiceCollectionExtensions
{
/// Registers worker process launcher and factory services.
/// Service collection to register services.
+ /// The same service collection, for chaining.
public static IServiceCollection AddWorkerProcessLauncher(this IServiceCollection services)
{
services.AddSingleton();
diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Alarms/AlarmFailoverEndToEndTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Alarms/AlarmFailoverEndToEndTests.cs
index 79b0797..f6b869c 100644
--- a/src/ZB.MOM.WW.MxGateway.Tests/Alarms/AlarmFailoverEndToEndTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Tests/Alarms/AlarmFailoverEndToEndTests.cs
@@ -32,6 +32,8 @@ public sealed class AlarmFailoverEndToEndTests
{
private static readonly TimeSpan WaitTimeout = TimeSpan.FromSeconds(30);
+ /// Verifies the alarm feed reflects each stage of a full alarmmgr-to-subtag failover and failback lifecycle.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task ProviderFailoverAndFailback_FullLifecycle_ReflectedInFeed()
{
@@ -227,6 +229,8 @@ public sealed class AlarmFailoverEndToEndTests
await monitor.StopAsync(CancellationToken.None);
}
+ /// Verifies a degraded transition cached before a new subscriber connects is replayed with its degraded flag and source provider intact.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task DegradedTransition_CachedThenReplayed_CarriesDegradedAndSourceProviderToNewSubscriber()
{
@@ -400,10 +404,13 @@ public sealed class AlarmFailoverEndToEndTests
public SubscribeAlarmsCommand? LastSubscribeCommand { get; private set; }
/// Pushes a worker event onto the monitor's event stream.
+ /// The worker event to push.
public void EmitEvent(MxEvent mxEvent) =>
_events.Writer.TryWrite(new WorkerEvent { Event = mxEvent });
/// Completes once the monitor has issued its SubscribeAlarms command.
+ /// The maximum time to wait.
+ /// A task that completes once the SubscribeAlarms command has been issued.
public Task WaitForSubscribeAsync(TimeSpan timeout) => _subscribed.Task.WaitAsync(timeout);
///
diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Alarms/AlarmWatchListResolverTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Alarms/AlarmWatchListResolverTests.cs
index d18edf0..f8c8b1b 100644
--- a/src/ZB.MOM.WW.MxGateway.Tests/Alarms/AlarmWatchListResolverTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Tests/Alarms/AlarmWatchListResolverTests.cs
@@ -39,6 +39,12 @@ public sealed class AlarmWatchListResolverTests
},
};
+ ///
+ /// Verifies Galaxy Repository rows and config includes are unioned, configured
+ /// excludes are removed, and duplicate references (case-insensitive) collapse
+ /// to a single entry.
+ ///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task ResolveAsync_UnionsGalaxyRowsAndIncludes_RemovesExcludes_AndDeduplicates()
{
@@ -63,6 +69,8 @@ public sealed class AlarmWatchListResolverTests
Assert.Equal(3, result.Count);
}
+ /// Verifies subtag item addresses are composed using the configured subtag names.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task ResolveAsync_ComposesSubtagAddressesFromConfigNames()
{
@@ -89,6 +97,8 @@ public sealed class AlarmWatchListResolverTests
Assert.Equal("Tank01.Level.HiHi.AckCmt", target.AckCommentSubtag);
}
+ /// Verifies that configuring empty Priority and AckComment subtag names leaves those target fields empty.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task ResolveAsync_EmptyPriorityAndAckComment_LeaveThoseFieldsEmpty()
{
@@ -110,6 +120,8 @@ public sealed class AlarmWatchListResolverTests
Assert.Equal(string.Empty, target.AckCommentSubtag);
}
+ /// Verifies the canonical full reference uses the row's discovered Galaxy area, not the configured Discovery.Area.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task ResolveAsync_ComposesCanonicalFullReference_FromRealGalaxyArea_NotConfigArea()
{
@@ -135,6 +147,8 @@ public sealed class AlarmWatchListResolverTests
Assert.Equal("Galaxy!TestArea.TestMachine_001.TestAlarm001", target.AlarmFullReference);
}
+ /// Verifies the canonical full reference omits the area segment when neither a discovered nor a configured area is present.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task ResolveAsync_ComposesCanonicalFullReference_WithoutArea()
{
@@ -153,6 +167,8 @@ public sealed class AlarmWatchListResolverTests
Assert.Equal("Galaxy!Tank01.Level.HiHi", target.AlarmFullReference);
}
+ /// Verifies a config include entry with no discovered area falls back to the configured Discovery.Area.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task ResolveAsync_ConfigInclude_UsesDiscoveryAreaFallback()
{
@@ -170,6 +186,8 @@ public sealed class AlarmWatchListResolverTests
Assert.Equal("Galaxy!Site_A.Tank01.Level.HiHi", target.AlarmFullReference);
}
+ /// Verifies a config include entry falls back to DefaultArea when Discovery.Area is empty.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task ResolveAsync_ConfigInclude_FallsBackToDefaultArea_WhenDiscoveryAreaEmpty()
{
@@ -187,6 +205,8 @@ public sealed class AlarmWatchListResolverTests
Assert.Equal("Galaxy!Plant.Tank01.Level.HiHi", target.AlarmFullReference);
}
+ /// Verifies that disabling Galaxy Repository discovery skips the repository call and resolves the watch list from configured includes only.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task ResolveAsync_UseGalaxyRepositoryFalse_DoesNotCallRepository_UsesIncludesOnly()
{
@@ -206,6 +226,8 @@ public sealed class AlarmWatchListResolverTests
Assert.Equal("Galaxy!Tank01.Level.HiHi", target.AlarmFullReference);
}
+ /// Verifies that a repository exception during discovery is logged and swallowed, returning the config-only watch list instead of throwing.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task ResolveAsync_RepositoryThrows_LogsAndReturnsConfigOnlySet()
{
@@ -221,6 +243,8 @@ public sealed class AlarmWatchListResolverTests
Assert.Equal("Galaxy!Tank01.Level.HiHi", target.AlarmFullReference);
}
+ /// Verifies the source object reference is derived from a config include entry, using the portion before the first dot when present.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task ResolveAsync_DerivesSourceObjectForConfigEntry()
{
@@ -240,6 +264,7 @@ public sealed class AlarmWatchListResolverTests
/// Fix 1: ExcludeAttributes must be ignored when UseGalaxyRepository is false.
/// A config-only include must survive even when the same path appears in ExcludeAttributes.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task ResolveAsync_ExcludeIgnored_WhenGalaxyRepositoryDisabled()
{
@@ -262,6 +287,7 @@ public sealed class AlarmWatchListResolverTests
/// Fix 1 (GR-on path): ExcludeAttributes still prunes GR rows when
/// UseGalaxyRepository is true.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task ResolveAsync_ExcludeApplied_WhenGalaxyRepositoryEnabled()
{
@@ -286,6 +312,7 @@ public sealed class AlarmWatchListResolverTests
/// Fix 1: A whitespace-only ExcludeAttributes entry must be skipped and must
/// not accidentally exclude any real reference.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task ResolveAsync_WhitespaceOnlyExcludeEntry_IsSkipped()
{
@@ -307,11 +334,12 @@ public sealed class AlarmWatchListResolverTests
}
///
- /// Server-051: a cancellation triggered while Galaxy Repository discovery is
+ /// A cancellation triggered while Galaxy Repository discovery is
/// awaiting must propagate as , not be
/// swallowed into a config-only watch-list, per the
/// contract.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task ResolveAsync_RepositoryCancelled_PropagatesOperationCanceled()
{
@@ -329,11 +357,12 @@ public sealed class AlarmWatchListResolverTests
}
///
- /// Server-052 item 2 / Server-053: an entry that appears in both
+ /// An entry that appears in both
/// IncludeAttributes and ExcludeAttributes is removed — excludes
/// win over explicit includes (the documented "excludes also suppress matching
/// explicit includes" behaviour).
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task ResolveAsync_ExcludeAlsoSuppressesMatchingExplicitInclude()
{
@@ -362,22 +391,32 @@ public sealed class AlarmWatchListResolverTests
/// Gets the number of times was called.
public int GetAlarmAttributesCount { get; private set; }
- ///
+ /// Always reports a successful connection.
+ /// Token to observe for cancellation.
+ /// A completed task carrying .
public Task TestConnectionAsync(CancellationToken ct = default) => Task.FromResult(true);
- ///
+ /// Reports no known deploy time.
+ /// Token to observe for cancellation.
+ /// A completed task carrying .
public Task GetLastDeployTimeAsync(CancellationToken ct = default) =>
Task.FromResult(null);
- ///
+ /// Not exercised by these tests; returns an empty hierarchy.
+ /// Token to observe for cancellation.
+ /// A completed task carrying an empty list.
public Task> GetHierarchyAsync(CancellationToken ct = default) =>
Task.FromResult(new List());
- ///
+ /// Not exercised by these tests; returns an empty attribute set.
+ /// Token to observe for cancellation.
+ /// A completed task carrying an empty list.
public Task> GetAttributesAsync(CancellationToken ct = default) =>
Task.FromResult(new List());
- ///
+ /// Returns the fixed alarm rowset supplied at construction, tracking how many times it was called.
+ /// Token to observe for cancellation.
+ /// A completed task carrying the configured alarm attribute rows.
public Task> GetAlarmAttributesAsync(CancellationToken ct = default)
{
GetAlarmAttributesCount++;
@@ -388,22 +427,32 @@ public sealed class AlarmWatchListResolverTests
/// whose alarm-attribute query throws.
private sealed class ThrowingGalaxyRepository(Exception toThrow) : IGalaxyRepository
{
- ///
+ /// Always reports a successful connection; only alarm-attribute discovery fails for this fake.
+ /// Token to observe for cancellation.
+ /// A completed task carrying .
public Task TestConnectionAsync(CancellationToken ct = default) => Task.FromResult(true);
- ///
+ /// Reports no known deploy time.
+ /// Token to observe for cancellation.
+ /// A completed task carrying .
public Task GetLastDeployTimeAsync(CancellationToken ct = default) =>
Task.FromResult(null);
- ///
+ /// Not exercised by these tests; returns an empty hierarchy.
+ /// Token to observe for cancellation.
+ /// A completed task carrying an empty list.
public Task> GetHierarchyAsync(CancellationToken ct = default) =>
Task.FromResult(new List());
- ///
+ /// Not exercised by these tests; returns an empty attribute set.
+ /// Token to observe for cancellation.
+ /// A completed task carrying an empty list.
public Task> GetAttributesAsync(CancellationToken ct = default) =>
Task.FromResult(new List());
- ///
+ /// Simulates the live SQL path failing by faulting with the configured exception.
+ /// Token to observe for cancellation.
+ /// A faulted task carrying the configured exception.
public Task> GetAlarmAttributesAsync(CancellationToken ct = default) =>
Task.FromException>(toThrow);
}
@@ -415,22 +464,32 @@ public sealed class AlarmWatchListResolverTests
///
private sealed class CancellingGalaxyRepository(CancellationTokenSource source) : IGalaxyRepository
{
- ///
+ /// Always reports a successful connection; only alarm-attribute discovery cancels for this fake.
+ /// Token to observe for cancellation.
+ /// A completed task carrying .
public Task TestConnectionAsync(CancellationToken ct = default) => Task.FromResult(true);
- ///
+ /// Reports no known deploy time.
+ /// Token to observe for cancellation.
+ /// A completed task carrying .
public Task GetLastDeployTimeAsync(CancellationToken ct = default) =>
Task.FromResult(null);
- ///
+ /// Not exercised by these tests; returns an empty hierarchy.
+ /// Token to observe for cancellation.
+ /// A completed task carrying an empty list.
public Task> GetHierarchyAsync(CancellationToken ct = default) =>
Task.FromResult(new List());
- ///
+ /// Not exercised by these tests; returns an empty attribute set.
+ /// Token to observe for cancellation.
+ /// A completed task carrying an empty list.
public Task> GetAttributesAsync(CancellationToken ct = default) =>
Task.FromResult(new List());
- ///
+ /// Cancels the supplied source and throws, mirroring the live SQL path being cancelled mid-await.
+ /// Token to observe for cancellation.
+ /// Never returns normally; always throws .
public Task> GetAlarmAttributesAsync(CancellationToken ct = default)
{
source.Cancel();
diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Alarms/GatewayAlarmMonitorProviderModeTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Alarms/GatewayAlarmMonitorProviderModeTests.cs
index 3fcdaa9..b3a2ba2 100644
--- a/src/ZB.MOM.WW.MxGateway.Tests/Alarms/GatewayAlarmMonitorProviderModeTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Tests/Alarms/GatewayAlarmMonitorProviderModeTests.cs
@@ -24,6 +24,8 @@ public sealed class GatewayAlarmMonitorProviderModeTests
{
private static readonly TimeSpan WaitTimeout = TimeSpan.FromSeconds(15);
+ /// A worker-reported provider-mode change to subtag broadcasts a degraded status message and increments the provider-switch metric.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task ProviderModeChange_BroadcastsDegradedStatus_AndIncrementsSwitchMetric()
{
@@ -121,12 +123,13 @@ public sealed class GatewayAlarmMonitorProviderModeTests
}
///
- /// Server-053: a redundant OnAlarmProviderModeChanged event whose target
+ /// A redundant OnAlarmProviderModeChanged event whose target
/// mode equals the current mode still records a provider switch. The worker is the
/// authority on when a mode change occurred; the gateway does not second-guess it,
/// so each event the worker emits increments provider_switches (no from==to
/// suppression). This test pins that semantics so it cannot drift silently.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task ProviderModeChange_RepeatedSameMode_RecordsASwitchForEachEvent()
{
@@ -219,7 +222,7 @@ public sealed class GatewayAlarmMonitorProviderModeTests
}
///
- /// Tests-032: pins the monitor's toMode → AlarmProviderSwitchReason
+ /// Pins the monitor's toMode → AlarmProviderSwitchReason
/// derivation (GatewayAlarmMonitor.ApplyProviderModeChangeAsync): an
/// alarmmgr→subtag change must emit reason=failover and a subtag→alarmmgr
/// change must emit reason=failback. Captures the reason tag off the
@@ -227,6 +230,7 @@ public sealed class GatewayAlarmMonitorProviderModeTests
/// the Failover/Failback arms or collapsed them to Unknown would be caught here,
/// whereas the count-only tests above would still pass.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task ProviderModeChange_FailoverThenFailback_RecordsCorrectReasonTags()
{
@@ -338,6 +342,8 @@ public sealed class GatewayAlarmMonitorProviderModeTests
await monitor.StopAsync(CancellationToken.None);
}
+ /// A subscriber that joins the alarm feed receives the current provider status as its first message.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task NewSubscriber_ReceivesProviderStatusAsFirstMessage()
{
@@ -374,6 +380,8 @@ public sealed class GatewayAlarmMonitorProviderModeTests
await monitor.StopAsync(CancellationToken.None);
}
+ /// With the fallback mode forced to subtag, the initial provider status baselines to degraded subtag without recording a provider switch.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task ForceSubtagConfig_BaselinesProviderStatusToSubtagDegraded_WithoutSwitch()
{
@@ -448,6 +456,8 @@ public sealed class GatewayAlarmMonitorProviderModeTests
await monitor.StopAsync(CancellationToken.None);
}
+ /// With the fallback mode forced to alarm manager, the initial provider status baselines to non-degraded alarmmgr without recording a provider switch.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task ForceAlarmManagerConfig_BaselinesProviderStatusToAlarmmgr_WithoutSwitch()
{
@@ -519,6 +529,8 @@ public sealed class GatewayAlarmMonitorProviderModeTests
await monitor.StopAsync(CancellationToken.None);
}
+ /// The monitor's SubscribeAlarms command carries the forced provider mode, failover settings, and watch list resolved from configuration.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task SubscribeAlarms_SendsForcedModeAndWatchList_FromConfiguration()
{
@@ -565,6 +577,10 @@ public sealed class GatewayAlarmMonitorProviderModeTests
await monitor.StopAsync(CancellationToken.None);
}
+ /// The configured fallback mode string maps to the expected sent in the SubscribeAlarms command.
+ /// The configured fallback mode string.
+ /// The expected forced .
+ /// A task that represents the asynchronous operation.
[Theory]
[InlineData("ForceAlarmManager", AlarmProviderMode.Alarmmgr)]
[InlineData("forcealarmmanager", AlarmProviderMode.Alarmmgr)]
@@ -701,10 +717,13 @@ public sealed class GatewayAlarmMonitorProviderModeTests
public SubscribeAlarmsCommand? LastSubscribeCommand { get; private set; }
/// Pushes a worker event onto the monitor's event stream.
+ /// The worker event to push.
public void EmitEvent(MxEvent mxEvent) =>
_events.Writer.TryWrite(new WorkerEvent { Event = mxEvent });
/// Completes once the monitor has issued its SubscribeAlarms command.
+ /// The maximum time to wait.
+ /// A task that represents the asynchronous operation.
public Task WaitForSubscribeAsync(TimeSpan timeout) => _subscribed.Task.WaitAsync(timeout);
///
diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Configuration/GatewayOptionsTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Configuration/GatewayOptionsTests.cs
index 7827252..25e08ac 100644
--- a/src/ZB.MOM.WW.MxGateway.Tests/Configuration/GatewayOptionsTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Tests/Configuration/GatewayOptionsTests.cs
@@ -123,12 +123,14 @@ public sealed class GatewayOptionsTests
StringComparison.Ordinal);
}
+ /// Verifies that defaults to .
[Fact]
public void DashboardOptions_DisableLogin_DefaultsToFalse()
{
Assert.False(new DashboardOptions().DisableLogin);
}
+ /// Verifies that defaults to .
[Fact]
public void DashboardOptions_AutoLoginUser_DefaultsToNull()
{
diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Configuration/GatewayOptionsValidatorTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Configuration/GatewayOptionsValidatorTests.cs
index 4d78263..a76b85b 100644
--- a/src/ZB.MOM.WW.MxGateway.Tests/Configuration/GatewayOptionsValidatorTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Tests/Configuration/GatewayOptionsValidatorTests.cs
@@ -46,6 +46,7 @@ public sealed class GatewayOptionsValidatorTests
Tls = tls,
};
+ /// Verifies default TLS options pass validation.
[Fact]
public void Validate_Succeeds_WithDefaultTlsOptions()
{
@@ -53,6 +54,7 @@ public sealed class GatewayOptionsValidatorTests
Assert.True(result.Succeeded);
}
+ /// Verifies a zero fails validation.
[Fact]
public void Validate_Fails_WhenTlsValidityYearsOutOfRange()
{
@@ -62,6 +64,7 @@ public sealed class GatewayOptionsValidatorTests
Assert.Contains(result.Failures!, f => f.Contains("MxGateway:Tls:ValidityYears"));
}
+ /// Verifies a above the allowed maximum fails validation.
[Fact]
public void Validate_Fails_WhenTlsValidityYearsTooLarge()
{
@@ -71,6 +74,7 @@ public sealed class GatewayOptionsValidatorTests
Assert.Contains(result.Failures!, f => f.Contains("MxGateway:Tls:ValidityYears"));
}
+ /// Verifies a blank entry in fails validation.
[Fact]
public void Validate_Fails_WhenAdditionalDnsNameBlank()
{
@@ -80,6 +84,7 @@ public sealed class GatewayOptionsValidatorTests
Assert.Contains(result.Failures!, f => f.Contains("MxGateway:Tls:AdditionalDnsNames"));
}
+ /// Verifies a blank fails validation.
[Fact]
public void Validate_Fails_WhenSelfSignedCertPathBlank()
{
@@ -89,6 +94,7 @@ public sealed class GatewayOptionsValidatorTests
Assert.Contains(result.Failures!, f => f.Contains("MxGateway:Tls:SelfSignedCertPath must not be blank."));
}
+ /// Verifies an LDAP port of zero fails validation.
[Fact]
public void Validate_Fails_WhenLdapPortIsZero()
{
@@ -100,6 +106,7 @@ public sealed class GatewayOptionsValidatorTests
f => f.Contains("MxGateway:Ldap:Port must be between 1 and 65535 (was 0)"));
}
+ /// Verifies an LDAP port above the allowed maximum fails validation.
[Fact]
public void Validate_Fails_WhenLdapPortExceedsMaximum()
{
@@ -111,6 +118,7 @@ public sealed class GatewayOptionsValidatorTests
f => f.Contains("MxGateway:Ldap:Port must be between 1 and 65535 (was 70000)"));
}
+ /// Verifies enabled LDAP with a valid port passes validation.
[Fact]
public void Validate_Succeeds_WhenLdapEnabledWithValidPort()
{
@@ -144,6 +152,7 @@ public sealed class GatewayOptionsValidatorTests
Tls = source.Tls,
};
+ /// Verifies an invalid fallback mode is not validated when alarms are disabled.
[Fact]
public void Validate_Succeeds_WhenAlarmsDisabled_FallbackNotValidated()
{
@@ -159,6 +168,7 @@ public sealed class GatewayOptionsValidatorTests
Assert.True(result.Succeeded);
}
+ /// Verifies the default "Auto" fallback mode passes validation when alarms are enabled.
[Fact]
public void Validate_Succeeds_WhenAlarmsEnabled_DefaultAutoConfig()
{
@@ -170,6 +180,8 @@ public sealed class GatewayOptionsValidatorTests
Assert.True(result.Succeeded);
}
+ /// Verifies each recognised (case-insensitive) fallback mode passes validation.
+ /// Fallback mode value under test.
[Theory]
[InlineData("Auto")]
[InlineData("ForceAlarmManager")]
@@ -184,6 +196,7 @@ public sealed class GatewayOptionsValidatorTests
Assert.True(result.Succeeded);
}
+ /// Verifies an unrecognised fallback mode fails validation when alarms are enabled.
[Fact]
public void Validate_Fails_WhenAlarmsEnabled_InvalidMode()
{
@@ -195,6 +208,7 @@ public sealed class GatewayOptionsValidatorTests
Assert.Contains(result.Failures!, f => f.Contains("MxGateway:Alarms:Fallback") && f.Contains("Mode"));
}
+ /// Verifies "ForceSubtag" fails validation without a Galaxy repository and without include attributes.
[Fact]
public void Validate_Fails_WhenForceSubtag_NoGalaxyRepository_NoIncludes()
{
@@ -217,6 +231,7 @@ public sealed class GatewayOptionsValidatorTests
f => f.Contains("ForceSubtag") && f.Contains("Discovery"));
}
+ /// Verifies "ForceSubtag" passes validation without a Galaxy repository when include attributes are supplied.
[Fact]
public void Validate_Succeeds_WhenForceSubtag_NoGalaxyRepository_WithIncludes()
{
@@ -236,6 +251,7 @@ public sealed class GatewayOptionsValidatorTests
Assert.True(result.Succeeded);
}
+ /// Verifies "ForceSubtag" passes validation when a Galaxy repository is used, even without include attributes.
[Fact]
public void Validate_Succeeds_WhenForceSubtag_WithGalaxyRepository()
{
@@ -251,6 +267,9 @@ public sealed class GatewayOptionsValidatorTests
Assert.True(result.Succeeded);
}
+ /// Verifies a non-positive fails validation.
+ /// Threshold value under test.
+ /// Configuration key fragment expected in the failure message.
[Theory]
[InlineData(0, nameof(AlarmFallbackOptions.ConsecutiveFailureThreshold))]
[InlineData(-1, nameof(AlarmFallbackOptions.ConsecutiveFailureThreshold))]
@@ -264,6 +283,9 @@ public sealed class GatewayOptionsValidatorTests
Assert.Contains(result.Failures!, f => f.Contains(keyPart));
}
+ /// Verifies a non-positive fails validation.
+ /// Interval value under test.
+ /// Configuration key fragment expected in the failure message.
[Theory]
[InlineData(0, nameof(AlarmFallbackOptions.FailbackProbeIntervalSeconds))]
[InlineData(-5, nameof(AlarmFallbackOptions.FailbackProbeIntervalSeconds))]
@@ -277,6 +299,9 @@ public sealed class GatewayOptionsValidatorTests
Assert.Contains(result.Failures!, f => f.Contains(keyPart));
}
+ /// Verifies a non-positive fails validation.
+ /// Probe count value under test.
+ /// Configuration key fragment expected in the failure message.
[Theory]
[InlineData(0, nameof(AlarmFallbackOptions.FailbackStableProbes))]
[InlineData(-1, nameof(AlarmFallbackOptions.FailbackStableProbes))]
@@ -308,6 +333,7 @@ public sealed class GatewayOptionsValidatorTests
Tls = source.Tls,
};
+ /// Verifies set to passes validation.
[Fact]
public void Validate_Succeeds_WhenAllowMultipleEventSubscribersIsTrue()
{
@@ -319,6 +345,8 @@ public sealed class GatewayOptionsValidatorTests
Assert.True(result.Succeeded);
}
+ /// Verifies a non-positive fails validation.
+ /// Subscriber cap value under test.
[Theory]
[InlineData(0)]
[InlineData(-1)]
@@ -334,6 +362,8 @@ public sealed class GatewayOptionsValidatorTests
f => f.Contains("MxGateway:Sessions:MaxEventSubscribersPerSession"));
}
+ /// Verifies a positive passes validation.
+ /// Subscriber cap value under test.
[Theory]
[InlineData(1)]
[InlineData(8)]
@@ -347,6 +377,7 @@ public sealed class GatewayOptionsValidatorTests
Assert.True(result.Succeeded);
}
+ /// Verifies default pass validation.
[Fact]
public void Validate_Succeeds_WithDefaultSessionOptions()
{
@@ -361,6 +392,7 @@ public sealed class GatewayOptionsValidatorTests
// WorkerReadyWaitTimeoutMs validation
// -------------------------------------------------------------------------
+ /// Verifies a negative fails validation.
[Fact]
public void Validate_Fails_WhenWorkerReadyWaitTimeoutMsIsNegative()
{
@@ -374,6 +406,7 @@ public sealed class GatewayOptionsValidatorTests
f => f.Contains("MxGateway:Sessions:WorkerReadyWaitTimeoutMs"));
}
+ /// Verifies a zero passes validation.
[Fact]
public void Validate_Succeeds_WhenWorkerReadyWaitTimeoutMsIsZero()
{
@@ -384,6 +417,7 @@ public sealed class GatewayOptionsValidatorTests
Assert.True(result.Succeeded);
}
+ /// Verifies a positive passes validation.
[Fact]
public void Validate_Succeeds_WhenWorkerReadyWaitTimeoutMsIsPositive()
{
@@ -394,6 +428,7 @@ public sealed class GatewayOptionsValidatorTests
Assert.True(result.Succeeded);
}
+ /// Verifies a negative fails validation.
[Fact]
public void Validate_Fails_WhenDetachGraceSecondsIsNegative()
{
@@ -407,6 +442,7 @@ public sealed class GatewayOptionsValidatorTests
f => f.Contains("MxGateway:Sessions:DetachGraceSeconds"));
}
+ /// Verifies a zero passes validation.
[Fact]
public void Validate_Succeeds_WhenDetachGraceSecondsIsZero()
{
@@ -435,6 +471,7 @@ public sealed class GatewayOptionsValidatorTests
Tls = source.Tls,
};
+ /// Verifies a negative fails validation.
[Fact]
public void Validate_Fails_WhenReplayBufferCapacityIsNegative()
{
@@ -448,6 +485,7 @@ public sealed class GatewayOptionsValidatorTests
f => f.Contains("MxGateway:Events:ReplayBufferCapacity"));
}
+ /// Verifies a zero passes validation.
[Fact]
public void Validate_Succeeds_WhenReplayBufferCapacityIsZero()
{
@@ -458,6 +496,7 @@ public sealed class GatewayOptionsValidatorTests
Assert.True(result.Succeeded);
}
+ /// Verifies a negative fails validation.
[Fact]
public void Validate_Fails_WhenReplayRetentionSecondsIsNegative()
{
@@ -471,6 +510,7 @@ public sealed class GatewayOptionsValidatorTests
f => f.Contains("MxGateway:Events:ReplayRetentionSeconds"));
}
+ /// Verifies a zero passes validation.
[Fact]
public void Validate_Succeeds_WhenReplayRetentionSecondsIsZero()
{
diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Configuration/TlsOptionsBindingTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Configuration/TlsOptionsBindingTests.cs
index 796a4ca..e315dfd 100644
--- a/src/ZB.MOM.WW.MxGateway.Tests/Configuration/TlsOptionsBindingTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Tests/Configuration/TlsOptionsBindingTests.cs
@@ -6,6 +6,7 @@ namespace ZB.MOM.WW.MxGateway.Tests.Configuration;
public sealed class TlsOptionsBindingTests
{
+ /// Verifies that applies its documented defaults when no configuration section is bound.
[Fact]
public void Defaults_AreApplied_WhenSectionAbsent()
{
@@ -16,6 +17,7 @@ public sealed class TlsOptionsBindingTests
Assert.False(string.IsNullOrWhiteSpace(options.SelfSignedCertPath));
}
+ /// Verifies that values bind correctly from the MxGateway:Tls configuration section.
[Fact]
public void Binds_FromMxGatewayTlsSection()
{
diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Contracts/ProtobufContractRoundTripTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Contracts/ProtobufContractRoundTripTests.cs
index 411dc3a..3cc8db8 100644
--- a/src/ZB.MOM.WW.MxGateway.Tests/Contracts/ProtobufContractRoundTripTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Tests/Contracts/ProtobufContractRoundTripTests.cs
@@ -347,8 +347,8 @@ public sealed class ProtobufContractRoundTripTests
/// by-name-specific payload case and instead reuses the
/// acknowledge_alarm ()
/// case. A future change that adds a separate by-name reply case — or
- /// drops the reuse — breaks this test. See Contracts-002 and
- /// docs/AlarmClientDiscovery.md section 4.
+ /// drops the reuse — breaks this test. See docs/AlarmClientDiscovery.md
+ /// section 4.
///
[Fact]
public void MxCommandReply_AcknowledgeAlarmByName_ReusesAcknowledgeAlarmPayloadCase()
@@ -1015,8 +1015,6 @@ public sealed class ProtobufContractRoundTripTests
/// Verifies that a round-trips,
/// pinning the credential-bearing entry shape
/// (current_user_id, verifier_user_id, value).
- /// See Contracts-011 for the credential-sensitivity comment on
- /// WriteSecuredBulkEntry.value.
///
[Fact]
public void WriteSecuredBulkCommand_RoundTripsCredentialBearingEntries()
@@ -1597,7 +1595,7 @@ public sealed class ProtobufContractRoundTripTests
/// with at least one
/// so a future renumber or type-narrowing is caught at the contract level.
/// Also verifies that an all-defaults (no elements)
- /// is not a proto-level error. See Contracts-023.
+ /// is not a proto-level error.
///
[Fact]
public void MxValue_RoundTripsSparseArrayValueAndPinsFieldNumbers()
diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Dashboard/DashboardBrowseServiceTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Dashboard/DashboardBrowseServiceTests.cs
index 7279215..4fd91cf 100644
--- a/src/ZB.MOM.WW.MxGateway.Tests/Dashboard/DashboardBrowseServiceTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Tests/Dashboard/DashboardBrowseServiceTests.cs
@@ -142,10 +142,14 @@ public sealed class DashboardBrowseServiceTests
/// Mutable so a single test can swap the entry mid-flight.
public GalaxyHierarchyCacheEntry Current { get; set; } = initial;
- ///
+ /// Does nothing; the stub never refreshes.
+ /// Cancellation token.
+ /// A task that represents the asynchronous operation.
public Task RefreshAsync(CancellationToken cancellationToken) => Task.CompletedTask;
- ///
+ /// Does nothing; the stub is always considered loaded.
+ /// Cancellation token.
+ /// A task that represents the asynchronous operation.
public Task WaitForFirstLoadAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
}
diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Diagnostics/AuthStoreHealthCheckTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Diagnostics/AuthStoreHealthCheckTests.cs
index df87ed3..f2ed7ff 100644
--- a/src/ZB.MOM.WW.MxGateway.Tests/Diagnostics/AuthStoreHealthCheckTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Tests/Diagnostics/AuthStoreHealthCheckTests.cs
@@ -12,6 +12,8 @@ public sealed class AuthStoreHealthCheckTests
return new AuthSqliteConnectionFactory(sqlitePath);
}
+ /// The health check reports healthy when the auth store database is reachable.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task Healthy_WhenStoreReachable()
{
@@ -25,6 +27,8 @@ public sealed class AuthStoreHealthCheckTests
finally { if (File.Exists(path)) File.Delete(path); }
}
+ /// The health check reports unhealthy when the database path cannot be opened.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task Unhealthy_WhenPathUnusable()
{
diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Diagnostics/GatewayLogRedactorSeamTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Diagnostics/GatewayLogRedactorSeamTests.cs
index 5265d4a..16f52c0 100644
--- a/src/ZB.MOM.WW.MxGateway.Tests/Diagnostics/GatewayLogRedactorSeamTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Tests/Diagnostics/GatewayLogRedactorSeamTests.cs
@@ -4,6 +4,7 @@ namespace ZB.MOM.WW.MxGateway.Tests.Diagnostics;
public sealed class GatewayLogRedactorSeamTests
{
+ /// Redacting a log property containing a bearer API key masks the secret while preserving the key id prefix.
[Fact]
public void Redact_MasksApiKeyInClientIdentity()
{
diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Galaxy/GalaxyFilterInputSafetyTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Galaxy/GalaxyFilterInputSafetyTests.cs
index 1939e30..ce957d8 100644
--- a/src/ZB.MOM.WW.MxGateway.Tests/Galaxy/GalaxyFilterInputSafetyTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Tests/Galaxy/GalaxyFilterInputSafetyTests.cs
@@ -9,16 +9,6 @@ namespace ZB.MOM.WW.MxGateway.Tests.Galaxy;
///
/// Adversarial-input coverage for the Galaxy Repository browse filter layer.
///
-/// Re-triage note (finding Tests-002): the Galaxy Repository's SQL surface
-/// (HierarchySql, AttributesSql, SELECT 1,
-/// SELECT time_of_last_deploy FROM galaxy) is entirely constant — no
-/// field is ever concatenated into a SQL
-/// string. All filters (TagNameGlob, RootTagName, category ids,
-/// template-chain filters, contained-path roots) are applied in memory by
-/// against the cached snapshot, so there is
-/// no SQL-injection surface and no LIKE-escaping helper to test.
-///
-///
/// The genuine, testable concern is that adversarial filter strings — SQL
/// metacharacters (', ;) and LIKE-wildcards (%,
/// _) — are treated as opaque literals by the in-memory filter layer:
@@ -41,6 +31,7 @@ public sealed class GalaxyFilterInputSafetyTests
];
/// Returns adversarial input cases for theory tests.
+ /// Theory data of adversarial input strings.
public static TheoryData AdversarialInputCases()
{
TheoryData data = [];
@@ -88,7 +79,7 @@ public sealed class GalaxyFilterInputSafetyTests
}
///
- /// Regression guard for finding Server-008: caches
+ /// caches
/// the compiled regex per glob pattern. Repeated calls with the same pattern, and
/// interleaved calls with different patterns, must keep returning the correct
/// literal-vs-wildcard result rather than a stale cached match.
@@ -229,6 +220,7 @@ public sealed class GalaxyFilterInputSafetyTests
/// zero matches rather than returning the whole hierarchy or faulting.
///
/// An adversarial glob containing SQL metacharacters or LIKE wildcards.
+ /// A task that represents the asynchronous operation.
[Theory]
[MemberData(nameof(AdversarialInputCases))]
public async Task DiscoverHierarchy_WithAdversarialTagNameGlob_ReturnsZeroMatches(string glob)
@@ -249,6 +241,7 @@ public sealed class GalaxyFilterInputSafetyTests
/// a query fragment or matching unrelated objects.
///
/// An adversarial tag name containing SQL metacharacters or LIKE wildcards.
+ /// A task that represents the asynchronous operation.
[Theory]
[MemberData(nameof(AdversarialInputCases))]
public async Task DiscoverHierarchy_WithAdversarialRootTagName_ReturnsNotFound(string rootTagName)
@@ -327,13 +320,17 @@ public sealed class GalaxyFilterInputSafetyTests
private sealed class StubGalaxyHierarchyCache(GalaxyHierarchyCacheEntry current) : IGalaxyHierarchyCache
{
- ///
+ /// Gets the fixed cache entry returned to callers.
public GalaxyHierarchyCacheEntry Current { get; } = current;
- ///
+ /// Does nothing; the stub never refreshes.
+ /// Cancellation token.
+ /// A task that represents the asynchronous operation.
public Task RefreshAsync(CancellationToken cancellationToken) => Task.CompletedTask;
- ///
+ /// Does nothing; the stub is always considered loaded.
+ /// Cancellation token.
+ /// A task that represents the asynchronous operation.
public Task WaitForFirstLoadAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardApiKeyManagementServiceTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardApiKeyManagementServiceTests.cs
index c27241d..f9c4a9d 100644
--- a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardApiKeyManagementServiceTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardApiKeyManagementServiceTests.cs
@@ -29,6 +29,7 @@ public sealed class DashboardApiKeyManagementServiceTests : IDisposable
private readonly List _tempDirectories = [];
/// Verifies that unauthorized users cannot create API keys.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task CreateAsync_UnauthorizedUser_DoesNotCreate()
{
@@ -45,6 +46,7 @@ public sealed class DashboardApiKeyManagementServiceTests : IDisposable
}
/// Verifies that authorized users create a verifiable, constrained key and audit it.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task CreateAsync_AuthorizedUser_CreatesVerifiableKeyAndAudits()
{
@@ -79,6 +81,7 @@ public sealed class DashboardApiKeyManagementServiceTests : IDisposable
}
/// Verifies that creating a key whose id already exists is rejected.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task CreateAsync_DuplicateKeyId_ReportsConflict()
{
@@ -96,6 +99,7 @@ public sealed class DashboardApiKeyManagementServiceTests : IDisposable
}
/// Verifies that authorized users can revoke keys with audit trail.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task RevokeAsync_AuthorizedUser_RevokesAndAudits()
{
@@ -121,6 +125,7 @@ public sealed class DashboardApiKeyManagementServiceTests : IDisposable
}
/// Verifies that authorized users can rotate a key's secret with audit trail.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task RotateAsync_AuthorizedUser_RotatesAndAudits()
{
@@ -153,6 +158,7 @@ public sealed class DashboardApiKeyManagementServiceTests : IDisposable
}
/// Verifies that authorized users can delete revoked keys with audit trail.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task DeleteAsync_AuthorizedUser_DeletesRevokedKeyAndAudits()
{
@@ -181,6 +187,7 @@ public sealed class DashboardApiKeyManagementServiceTests : IDisposable
/// dashboard-delete-key audit entry with Details = "not-found-or-active" is still
/// written — audit completeness for refused deletes.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task DeleteAsync_ActiveKey_ReportsFriendlyErrorAndAudits()
{
@@ -206,6 +213,7 @@ public sealed class DashboardApiKeyManagementServiceTests : IDisposable
/// A blank key id fails validation before any store or audit call runs.
/// A blank or whitespace key identifier.
+ /// A task that represents the asynchronous operation.
[Theory]
[InlineData("")]
[InlineData(" ")]
@@ -225,10 +233,11 @@ public sealed class DashboardApiKeyManagementServiceTests : IDisposable
}
///
- /// Server-004 regression: the dashboard create path must reject a request carrying a
+ /// The dashboard create path must reject a request carrying a
/// non-canonical scope string rather than persisting a key whose scope the authorization
/// resolver never matches.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task CreateAsync_UnknownScope_DoesNotCreate()
{
@@ -255,6 +264,7 @@ public sealed class DashboardApiKeyManagementServiceTests : IDisposable
/// Phase 3 canonical audit shape: the dashboard-create-key canonical AuditEvent records
/// the operator username as Actor and the managed keyId as Target.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task CreateAsync_AuthorizedUser_CanonicalAuditEventHasOperatorAsActorAndKeyIdAsTarget()
{
@@ -386,7 +396,10 @@ public sealed class DashboardApiKeyManagementServiceTests : IDisposable
/// Gets the recorded canonical audit events.
public List Events { get; } = [];
- ///
+ /// Records the audit event in memory instead of writing it to a real sink.
+ /// The audit event to record.
+ /// A token to observe for cancellation requests.
+ /// A task that represents the asynchronous operation.
public Task WriteAsync(ZB.MOM.WW.Audit.AuditEvent auditEvent, CancellationToken cancellationToken = default)
{
Events.Add(auditEvent);
diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardAuthenticatorTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardAuthenticatorTests.cs
index 7cd03a6..08e87b1 100644
--- a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardAuthenticatorTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardAuthenticatorTests.cs
@@ -20,6 +20,8 @@ namespace ZB.MOM.WW.MxGateway.Tests.Gateway.Dashboard;
public sealed class DashboardAuthenticatorTests
{
/// A blank username is rejected without touching the LDAP provider.
+ /// The blank/whitespace/null username under test.
+ /// A task that represents the asynchronous operation.
[Theory]
[InlineData(null)]
[InlineData("")]
@@ -40,6 +42,7 @@ public sealed class DashboardAuthenticatorTests
}
/// A blank password is rejected without touching the LDAP provider.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task AuthenticateAsync_BlankPassword_FailsWithoutCallingLdap()
{
@@ -60,6 +63,8 @@ public sealed class DashboardAuthenticatorTests
/// A failed LDAP outcome (any failure bucket, including )
/// maps to the generic dashboard failure without leaking the raw password.
///
+ /// The LDAP failure bucket under test.
+ /// A task that represents the asynchronous operation.
[Theory]
[InlineData(LdapAuthFailure.Disabled)]
[InlineData(LdapAuthFailure.BadCredentials)]
@@ -87,6 +92,7 @@ public sealed class DashboardAuthenticatorTests
/// name (ClaimTypes.Name), and the username (ClaimTypes.NameIdentifier), under the
/// dashboard authentication scheme.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task AuthenticateAsync_Success_BuildsPrincipalWithExpectedClaims()
{
@@ -127,11 +133,12 @@ public sealed class DashboardAuthenticatorTests
}
///
- /// Task 1.5: the principal emits canonical ZbClaimTypes.Username ("zb:username") with
+ /// The principal emits canonical ZbClaimTypes.Username ("zb:username") with
/// the login username, ZbClaimTypes.Role (= ClaimTypes.Role) for each resolved role, and
/// ZbClaimTypes.DisplayName ("zb:displayname") with the display name — while keeping
/// ClaimTypes.NameIdentifier, ClaimTypes.Name, and mxgateway:ldap_group claims intact.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task AuthenticateAsync_Success_EmitsCanonicalZbClaims()
{
@@ -183,6 +190,7 @@ public sealed class DashboardAuthenticatorTests
/// When the user authenticates but none of their groups map to a dashboard role,
/// the login is denied (the long-standing "no roles matched → denied" rule).
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task AuthenticateAsync_NoRolesMatched_DeniesLogin()
{
@@ -216,6 +224,7 @@ public sealed class DashboardAuthenticatorTests
/// for the realistic (already-short) group shape.
///
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task AuthenticateAsync_GroupAsDistinguishedNameFromService_ResolvesRoleAndSurfacesServiceValue()
{
@@ -243,6 +252,7 @@ public sealed class DashboardAuthenticatorTests
}
/// The (already-trimmed) username from the LDAP result flows onto the principal.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task AuthenticateAsync_UsesUsernameFromLdapResult()
{
@@ -296,10 +306,17 @@ public sealed class DashboardAuthenticatorTests
///
private sealed class FakeLdapAuthService(LdapAuthResult result) : ILdapAuthService
{
+ /// Gets a value indicating whether was invoked.
public bool WasCalled { get; private set; }
+ /// Gets the username passed to the most recent call.
public string? LastUsername { get; private set; }
+ /// Records the call and returns the configured fixed result.
+ /// The username to authenticate.
+ /// The password to authenticate.
+ /// Token to observe for cancellation.
+ /// The fixed configured for this fake.
public Task AuthenticateAsync(string username, string password, CancellationToken ct)
{
WasCalled = true;
diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardAuthorizationHandlerTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardAuthorizationHandlerTests.cs
index 6dbf84a..9d4ab2d 100644
--- a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardAuthorizationHandlerTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardAuthorizationHandlerTests.cs
@@ -11,6 +11,7 @@ namespace ZB.MOM.WW.MxGateway.Tests.Gateway.Dashboard;
public sealed class DashboardAuthorizationHandlerTests
{
/// Verifies that unauthenticated remote requests fail authorization.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task HandleAsync_UnauthenticatedRemoteRequest_DoesNotSucceed()
{
@@ -24,6 +25,7 @@ public sealed class DashboardAuthorizationHandlerTests
}
/// Verifies that anonymous localhost access succeeds when allowed.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task HandleAsync_AnonymousLocalhostAllowed_Succeeds()
{
@@ -40,6 +42,7 @@ public sealed class DashboardAuthorizationHandlerTests
/// Verifies that the anonymous-localhost bypass is denied when AllowAnonymousLocalhost
/// is off, even on a loopback connection — the misconfiguration must not expose the dashboard.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task HandleAsync_AnonymousLocalhostDisallowed_DoesNotSucceed()
{
@@ -56,6 +59,7 @@ public sealed class DashboardAuthorizationHandlerTests
/// Verifies that the anonymous-localhost bypass stays scoped to loopback: an anonymous
/// request from a non-loopback address is denied even when AllowAnonymousLocalhost is on.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task HandleAsync_AnonymousLocalhostAllowedFromRemoteAddress_DoesNotSucceed()
{
@@ -69,6 +73,7 @@ public sealed class DashboardAuthorizationHandlerTests
}
/// Verifies that an authenticated user without any dashboard role fails the viewer requirement.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task HandleAsync_AuthenticatedWithoutDashboardRole_DoesNotSucceed()
{
@@ -82,6 +87,7 @@ public sealed class DashboardAuthorizationHandlerTests
}
/// Verifies that a Viewer satisfies the viewer-or-admin requirement.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task HandleAsync_ViewerRole_SatisfiesViewerPolicy()
{
@@ -95,6 +101,7 @@ public sealed class DashboardAuthorizationHandlerTests
}
/// Verifies that an Admin satisfies the admin-only requirement.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task HandleAsync_AdminRole_SatisfiesAdminPolicy()
{
@@ -108,6 +115,7 @@ public sealed class DashboardAuthorizationHandlerTests
}
/// Verifies that a Viewer does NOT satisfy the admin-only requirement.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task HandleAsync_ViewerRole_DoesNotSatisfyAdminPolicy()
{
diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardAutoLoginAuthenticationHandlerTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardAutoLoginAuthenticationHandlerTests.cs
index 219e031..73903e9 100644
--- a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardAutoLoginAuthenticationHandlerTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardAutoLoginAuthenticationHandlerTests.cs
@@ -5,6 +5,7 @@ namespace ZB.MOM.WW.MxGateway.Tests.Gateway.Dashboard;
public sealed class DashboardAutoLoginAuthenticationHandlerTests
{
+ /// The default auto-login user mints an authenticated principal with both dashboard roles.
[Fact]
public void CreatePrincipal_MintsAuthenticatedMultiRoleUser()
{
@@ -16,6 +17,8 @@ public sealed class DashboardAutoLoginAuthenticationHandlerTests
Assert.True(principal.IsInRole(DashboardRoles.Viewer));
}
+ /// A null, empty, or whitespace-only user falls back to the configured default user.
+ /// The blank/whitespace/null user under test.
[Theory]
[InlineData(null)]
[InlineData("")]
@@ -27,6 +30,7 @@ public sealed class DashboardAutoLoginAuthenticationHandlerTests
Assert.Equal(DashboardAutoLoginAuthenticationHandler.DefaultUser, principal.Identity!.Name);
}
+ /// Leading/trailing whitespace around the user name is trimmed before minting the principal.
[Fact]
public void CreatePrincipal_TrimsUser()
{
@@ -35,6 +39,7 @@ public sealed class DashboardAutoLoginAuthenticationHandlerTests
Assert.Equal("multi-role", principal.Identity!.Name);
}
+ /// A custom user name preserves its identity name and still receives both dashboard roles.
[Fact]
public void CreatePrincipal_CustomUser_PreservesNameAndRoles()
{
diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardBrowseAndAlarmModelTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardBrowseAndAlarmModelTests.cs
index f3efc6b..4aab95a 100644
--- a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardBrowseAndAlarmModelTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardBrowseAndAlarmModelTests.cs
@@ -171,13 +171,11 @@ public sealed class DashboardBrowseAndAlarmModelTests
Assert.True(model.IsDegraded);
Assert.Contains("bg-warning", model.BadgeCssClass, StringComparison.Ordinal);
Assert.Equal("x", model.Reason);
- // Tests-033: pin the amber label text, not just the CSS class — a label swap
- // would otherwise pass this test.
Assert.Equal(DashboardAlarmProviderStatus.DegradedLabel, model.Label);
}
///
- /// Tests-033: an explicitly-degraded status whose mode is still Alarmmgr (the
+ /// An explicitly-degraded status whose mode is still Alarmmgr (the
/// Degraded || Mode==Subtag guard's second, independent branch) must still
/// map to the degraded amber badge.
///
@@ -199,7 +197,7 @@ public sealed class DashboardBrowseAndAlarmModelTests
}
///
- /// Tests-033: the SinceUtc field must carry the protobuf Since
+ /// The SinceUtc field must carry the protobuf Since
/// timestamp converted to a .
///
[Fact]
@@ -220,7 +218,7 @@ public sealed class DashboardBrowseAndAlarmModelTests
}
///
- /// Tests-033: — the entry the
+ /// — the entry the
/// dashboard SignalR snapshot path actually calls — projects a provider-status
/// feed message into the badge model.
///
@@ -246,7 +244,7 @@ public sealed class DashboardBrowseAndAlarmModelTests
}
///
- /// Tests-033: throws
+ /// throws
/// when the feed message does not carry a
/// provider-status payload.
///
diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardCookieOptionsTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardCookieOptionsTests.cs
index 40fcfea..435a859 100644
--- a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardCookieOptionsTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardCookieOptionsTests.cs
@@ -11,6 +11,7 @@ namespace ZB.MOM.WW.MxGateway.Tests.Gateway.Dashboard;
public sealed class DashboardCookieOptionsTests
{
/// Verifies that the application configures secure dashboard authentication cookies.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task Build_ConfiguresSecureDashboardCookie()
{
@@ -36,6 +37,7 @@ public sealed class DashboardCookieOptionsTests
/// relaxes the cookie to so
/// the dashboard can be reached over plain HTTP in dev.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task Build_WithRequireHttpsCookieFalse_UsesSameAsRequest()
{
@@ -55,6 +57,7 @@ public sealed class DashboardCookieOptionsTests
/// cookie name, so a gateway instance sharing a hostname with another can be given a
/// distinct name (browser cookies are scoped by host+path, not port).
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task Build_WithCookieNameOverride_UsesConfiguredName()
{
diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardDisableLoginTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardDisableLoginTests.cs
index b4a3fa5..f40f863 100644
--- a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardDisableLoginTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardDisableLoginTests.cs
@@ -11,6 +11,8 @@ namespace ZB.MOM.WW.MxGateway.Tests.Gateway.Dashboard;
public sealed class DashboardDisableLoginTests
{
+ /// Verifies that when dashboard login is not disabled, the dashboard authentication scheme resolves to the cookie authentication handler.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task DisableLoginOff_CookieSchemeUsesCookieHandler()
{
@@ -25,6 +27,8 @@ public sealed class DashboardDisableLoginTests
Assert.Equal(typeof(CookieAuthenticationHandler), scheme!.HandlerType);
}
+ /// Verifies that when MxGateway:Dashboard:DisableLogin is enabled, the dashboard authentication scheme resolves to the auto-login handler instead of the cookie handler.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task DisableLoginOn_CookieSchemeUsesAutoLoginHandler()
{
@@ -40,6 +44,8 @@ public sealed class DashboardDisableLoginTests
Assert.Equal(typeof(DashboardAutoLoginAuthenticationHandler), scheme!.HandlerType);
}
+ /// Verifies that the auto-login principal created when login is disabled satisfies the Admin, Viewer, and hub-clients authorization policies.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task DisableLoginOn_AutoLoginPrincipalSatisfiesAdminAndViewerPolicies()
{
diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardGalaxySummaryProjectorTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardGalaxySummaryProjectorTests.cs
index 2a3879e..7447a77 100644
--- a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardGalaxySummaryProjectorTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardGalaxySummaryProjectorTests.cs
@@ -130,6 +130,8 @@ public sealed class DashboardGalaxySummaryProjectorTests
/// Verifies that the cache status enum is faithfully mapped to the dashboard
/// status enum across every defined value.
///
+ /// The Galaxy cache status to project.
+ /// The expected dashboard status for the given cache status.
[Theory]
[InlineData(GalaxyCacheStatus.Healthy, DashboardGalaxyStatus.Healthy)]
[InlineData(GalaxyCacheStatus.Stale, DashboardGalaxyStatus.Stale)]
diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardGroupRoleMapperTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardGroupRoleMapperTests.cs
index 935f7c7..480f483 100644
--- a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardGroupRoleMapperTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardGroupRoleMapperTests.cs
@@ -35,6 +35,7 @@ public sealed class DashboardGroupRoleMapperTests
/// Verifies full-DN match, leading-RDN fallback, case-insensitivity, and unmapped → empty.
/// The LDAP group name or distinguished name.
/// The expected role or null if no match.
+ /// A task that represents the asynchronous operation.
[Theory]
[InlineData("GwAdmin", DashboardRoles.Admin)]
[InlineData("gwadmin", DashboardRoles.Admin)]
@@ -61,6 +62,7 @@ public sealed class DashboardGroupRoleMapperTests
}
/// Verifies that admin and viewer roles are both emitted when both groups are present.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task MapAsync_AdminPlusViewer_BothRolesEmitted()
{
@@ -75,6 +77,7 @@ public sealed class DashboardGroupRoleMapperTests
}
/// Verifies that an empty GroupToRole map yields no roles.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task MapAsync_EmptyMapping_ReturnsNoRoles()
{
@@ -86,12 +89,13 @@ public sealed class DashboardGroupRoleMapperTests
}
///
- /// Task 1.7 (canonical roles): an LDAP user in the admin group must resolve
+ /// Canonical roles: an LDAP user in the admin group must resolve
/// to the canonical role value "Administrator" (not the legacy
/// "Admin"), and the reader group to "Viewer". Asserted with
/// string LITERALS — independent of — so a
/// regression on the constant's value is caught here.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task MapAsync_AdminGroup_ResolvesToCanonicalAdministratorValue()
{
@@ -105,7 +109,7 @@ public sealed class DashboardGroupRoleMapperTests
}
///
- /// Task 1.7: the canonical admin value ("Administrator") passes the
+ /// The canonical admin value ("Administrator") passes the
/// admin-only gate while a "Viewer" fails it — asserted with literals,
/// proving enforcement is bound to the new value and the legacy "Admin"
/// string is no longer what authorizes admin actions.
@@ -124,6 +128,7 @@ public sealed class DashboardGroupRoleMapperTests
/// Verifies the extracted shared helper is the single source of truth: it
/// produces the same roles the mapper does for the same inputs.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task SharedHelper_MatchesMapperOutput()
{
diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardHubsRegistrationTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardHubsRegistrationTests.cs
index 3348029..638dbd5 100644
--- a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardHubsRegistrationTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardHubsRegistrationTests.cs
@@ -10,6 +10,7 @@ namespace ZB.MOM.WW.MxGateway.Tests.Gateway.Dashboard;
public sealed class DashboardHubsRegistrationTests
{
/// Verifies that dashboard build maps all three hubs and token endpoint.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task Build_WhenDashboardEnabled_MapsAllThreeHubsAndTokenEndpoint()
{
@@ -27,6 +28,7 @@ public sealed class DashboardHubsRegistrationTests
}
/// Verifies that dashboard build registers hub token service and connection factory.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task Build_WhenDashboardEnabled_RegistersHubTokenServiceAndConnectionFactory()
{
diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardSessionAdminServiceTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardSessionAdminServiceTests.cs
index 216874c..126edf1 100644
--- a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardSessionAdminServiceTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardSessionAdminServiceTests.cs
@@ -10,6 +10,7 @@ namespace ZB.MOM.WW.MxGateway.Tests.Gateway.Dashboard;
public sealed class DashboardSessionAdminServiceTests
{
/// Verifies that a viewer cannot close a session.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task CloseSessionAsync_ViewerCannotManage()
{
@@ -26,6 +27,7 @@ public sealed class DashboardSessionAdminServiceTests
}
/// Verifies that an admin can close a session.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task CloseSessionAsync_AdminClosesSession()
{
@@ -43,6 +45,7 @@ public sealed class DashboardSessionAdminServiceTests
}
/// Verifies that closing a missing session returns a friendly error message.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task CloseSessionAsync_WhenSessionMissing_ReportsFriendlyError()
{
@@ -62,6 +65,7 @@ public sealed class DashboardSessionAdminServiceTests
}
/// Verifies that a viewer cannot kill a worker.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task KillWorkerAsync_ViewerCannotManage()
{
@@ -78,6 +82,7 @@ public sealed class DashboardSessionAdminServiceTests
}
/// Verifies that an admin can kill a worker.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task KillWorkerAsync_AdminKillsWorker()
{
@@ -93,14 +98,11 @@ public sealed class DashboardSessionAdminServiceTests
Assert.Equal(1, sessionManager.KillCount);
Assert.Equal("session-1", sessionManager.LastKilledSessionId);
- // Tests-028: pin the literal reason string so a future caller-side change is a deliberate
- // test update rather than a silent drift. DashboardSessionAdminService passes a hard-coded
- // "dashboard-admin-kill" so the worker-exit metric (mxgateway.workers.killed) carries a
- // stable, machine-greppable reason tag.
Assert.Equal("dashboard-admin-kill", sessionManager.LastKillReason);
}
/// Verifies that killing a worker with a blank session ID returns failure.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task KillWorkerAsync_BlankSessionId_ReturnsFailure()
{
@@ -117,10 +119,11 @@ public sealed class DashboardSessionAdminServiceTests
}
///
- /// Tests-029: CloseSessionAsync has the same blank-session-id guard as
+ /// CloseSessionAsync has the same blank-session-id guard as
/// KillWorkerAsync but previously had no parallel test. Coverage was asymmetric.
/// A guard-removal regression on the close path would slip through.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task CloseSessionAsync_BlankSessionId_ReturnsFailure()
{
@@ -148,12 +151,13 @@ public sealed class DashboardSessionAdminServiceTests
}
///
- /// Regression for Server-050: an unexpected (non-)
+ /// Regression: an unexpected (non-)
/// exception from CloseSessionAsync — e.g. an
/// or surfaced from RemoveSessionAsync/DisposeAsync —
/// must be converted to a friendly
/// rather than propagating raw into Blazor's error boundary.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task CloseSessionAsync_WhenManagerThrowsUnexpected_ReturnsFriendlyFail()
{
@@ -173,8 +177,9 @@ public sealed class DashboardSessionAdminServiceTests
}
///
- /// Regression for Server-050: same friendly-fail contract for the Kill path.
+ /// Regression: same friendly-fail contract for the Kill path.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task KillWorkerAsync_WhenManagerThrowsUnexpected_ReturnsFriendlyFail()
{
diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardSnapshotPublisherTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardSnapshotPublisherTests.cs
index 6533a84..742aa3f 100644
--- a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardSnapshotPublisherTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardSnapshotPublisherTests.cs
@@ -11,20 +11,14 @@ public sealed class DashboardSnapshotPublisherTests
private static readonly TimeSpan TestTimeout = TimeSpan.FromSeconds(5);
///
- /// Server-042 regression: a transient failure inside
+ /// A transient failure inside
/// must not
/// end the BackgroundService; the publisher must wait the configured
/// reconnect delay and then re-open the subscription. Before the fix,
/// the publisher exited on the first non-cancellation exception and
/// the dashboard's snapshot stream went silent until process restart.
- ///
- /// Tests-031: the reconnect-gap measurement is bounded between the
- /// moment the first subscribe actually throws and the moment the
- /// second subscribe begins. Measuring from startedAt (pre-StartAsync)
- /// baselined scheduling overhead into the budget and made the lower bound
- /// flaky on slow CI; recording firstThrowAt inside the fake removes
- /// that baseline so only the Task.Delay(reconnectDelay) contributes.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task ExecuteAsync_WhenSnapshotServiceThrowsOnce_ReconnectsAfterDelay()
{
@@ -60,12 +54,6 @@ public sealed class DashboardSnapshotPublisherTests
$"Expected at least 2 subscribe calls, got {snapshotService.SubscribeCount}.");
Assert.True(hubContext.SendCount >= 1);
- // Tests-031: the gap is measured from the moment the first subscribe
- // actually threw (inside the fake) to the moment the second subscribe
- // began (also inside the fake). This isolates the publisher's
- // Task.Delay(reconnectDelay) — no StartAsync / scheduling overhead in
- // the baseline. The 10ms slack absorbs Task.Delay's coarse Windows
- // timer quantum (~15ms) when the underlying scheduler wakes early.
TimeSpan gap = secondSubscribeAt - firstThrowAt;
Assert.True(gap >= reconnectDelay - TimeSpan.FromMilliseconds(10),
$"Expected reconnect gap >= {reconnectDelay.TotalMilliseconds}ms; got {gap.TotalMilliseconds}ms.");
@@ -75,6 +63,7 @@ public sealed class DashboardSnapshotPublisherTests
/// Sanity: a normal completion of WatchSnapshotsAsync (no exception)
/// also reconnects after the delay — exits only on host shutdown.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task ExecuteAsync_WhenSnapshotServiceCompletes_ReconnectsAfterDelay()
{
@@ -114,7 +103,7 @@ public sealed class DashboardSnapshotPublisherTests
public int SubscribeCount { get; private set; }
///
- /// Tests-031: the wall-clock instant the first WatchSnapshotsAsync throws.
+ /// The wall-clock instant the first WatchSnapshotsAsync throws.
/// The reconnect-gap assertion is measured against this timestamp (NOT the
/// pre-StartAsync wall clock) so scheduling overhead is not baselined
/// into the lower bound.
@@ -124,14 +113,13 @@ public sealed class DashboardSnapshotPublisherTests
/// Gets the wall-clock instant of the second subscription attempt.
public DateTimeOffset? SecondSubscribeAt { get; private set; }
- /// Gets the current snapshot.
+ ///
public DashboardSnapshot GetSnapshot()
{
return null!;
}
- /// Watches for snapshot changes and yields them asynchronously.
- /// Token to observe for cancellation.
+ ///
public async IAsyncEnumerable WatchSnapshotsAsync(
[EnumeratorCancellation] CancellationToken cancellationToken)
{
@@ -167,14 +155,13 @@ public sealed class DashboardSnapshotPublisherTests
/// Gets the number of subscription attempts.
public int SubscribeCount { get; private set; }
- /// Gets the current snapshot.
+ ///
public DashboardSnapshot GetSnapshot()
{
return null!;
}
- /// Watches for snapshot changes and completes immediately.
- /// Token to observe for cancellation.
+ ///
#pragma warning disable CS1998 // async without await — IAsyncEnumerable contract requires async signature
public async IAsyncEnumerable WatchSnapshotsAsync(
[EnumeratorCancellation] CancellationToken cancellationToken)
@@ -211,35 +198,43 @@ public sealed class DashboardSnapshotPublisherTests
/// Gets a client proxy excluding specified connections.
/// Connection identifiers to exclude.
+ /// The recording client proxy shared by this fake.
public IClientProxy AllExcept(IReadOnlyList excludedConnectionIds) => AllProxy;
/// Gets a client proxy for a specific connection.
/// The connection identifier.
+ /// The recording client proxy shared by this fake.
public IClientProxy Client(string connectionId) => AllProxy;
/// Gets a client proxy for specified connections.
/// The connection identifiers.
+ /// The recording client proxy shared by this fake.
public IClientProxy Clients(IReadOnlyList connectionIds) => AllProxy;
/// Gets a client proxy for a group.
/// The group name.
+ /// The recording client proxy shared by this fake.
public IClientProxy Group(string groupName) => AllProxy;
/// Gets a client proxy for a group excluding specified connections.
/// The group name.
/// Connection identifiers to exclude.
+ /// The recording client proxy shared by this fake.
public IClientProxy GroupExcept(string groupName, IReadOnlyList excludedConnectionIds) => AllProxy;
/// Gets a client proxy for specified groups.
/// The group names.
+ /// The recording client proxy shared by this fake.
public IClientProxy Groups(IReadOnlyList groupNames) => AllProxy;
/// Gets a client proxy for a specific user.
/// The user identifier.
+ /// The recording client proxy shared by this fake.
public IClientProxy User(string userId) => AllProxy;
/// Gets a client proxy for specified users.
/// The user identifiers.
+ /// The recording client proxy shared by this fake.
public IClientProxy Users(IReadOnlyList userIds) => AllProxy;
}
@@ -254,6 +249,7 @@ public sealed class DashboardSnapshotPublisherTests
/// The SignalR method name.
/// The method arguments.
/// Token to observe for cancellation.
+ /// A task that represents the asynchronous operation.
public Task SendCoreAsync(string method, object?[] args, CancellationToken cancellationToken = default)
{
Interlocked.Increment(ref _sendCount);
@@ -267,6 +263,7 @@ public sealed class DashboardSnapshotPublisherTests
/// The connection identifier.
/// The group name.
/// Token to observe for cancellation.
+ /// A task that represents the asynchronous operation.
public Task AddToGroupAsync(string connectionId, string groupName, CancellationToken cancellationToken = default)
=> Task.CompletedTask;
@@ -274,6 +271,7 @@ public sealed class DashboardSnapshotPublisherTests
/// The connection identifier.
/// The group name.
/// Token to observe for cancellation.
+ /// A task that represents the asynchronous operation.
public Task RemoveFromGroupAsync(string connectionId, string groupName, CancellationToken cancellationToken = default)
=> Task.CompletedTask;
}
diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardSnapshotServiceTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardSnapshotServiceTests.cs
index 4df7ce3..50698aa 100644
--- a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardSnapshotServiceTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardSnapshotServiceTests.cs
@@ -281,7 +281,7 @@ public sealed class DashboardSnapshotServiceTests
}
///
- /// Regression for Server-059: the shared library replaces the cache entry via
+ /// The shared library replaces the cache entry via
/// previous with { ... } at the same Sequence on a steady-state tick and on a
/// refresh failure (Status → Unavailable, LastError set). The dashboard summary must reflect
/// those per-tick-volatile fields, not a summary frozen at the last heavy-refresh sequence —
@@ -356,7 +356,7 @@ public sealed class DashboardSnapshotServiceTests
///
/// Verifies that a changed cache Sequence invalidates the memoized template breakdown and the
/// next snapshot reflects the new object set. Guards against an inverted sequence check that
- /// would freeze the Galaxy section after its first load (Tests-041).
+ /// would freeze the Galaxy section after its first load.
///
[Fact]
public void GetSnapshot_WhenSequenceChanges_RecomputesTemplateBreakdown()
@@ -405,6 +405,7 @@ public sealed class DashboardSnapshotServiceTests
}
/// Verifies that snapshot service refreshes API key summaries before each snapshot.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task WatchSnapshotsAsync_RefreshesApiKeySummariesBeforeSnapshot()
{
@@ -441,6 +442,7 @@ public sealed class DashboardSnapshotServiceTests
}
/// Verifies that snapshot service reuses previous summaries when API key refresh fails.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task WatchSnapshotsAsync_WhenApiKeyRefreshFails_ReusesPreviousSummaries()
{
@@ -484,6 +486,7 @@ public sealed class DashboardSnapshotServiceTests
}
/// Verifies that snapshot service disposes cleanly when subscriber cancels.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task WatchSnapshotsAsync_WhenSubscriberCancels_DisposesCleanly()
{
@@ -575,31 +578,47 @@ public sealed class DashboardSnapshotServiceTests
private class FakeApiKeyAdminStore : IApiKeyAdminStore
{
- ///
+ /// Does nothing; the fake never persists created keys.
+ /// API key record to create.
+ /// Cancellation token.
+ /// A task that represents the asynchronous operation.
public Task CreateAsync(ApiKeyRecord record, CancellationToken ct)
{
return Task.CompletedTask;
}
- ///
+ /// Lists API keys; the base fake always returns an empty list.
+ /// Cancellation token.
+ /// An empty list of API key summaries.
public virtual Task> ListAsync(CancellationToken ct)
{
return Task.FromResult>([]);
}
- ///
+ /// Does nothing; the fake never revokes keys.
+ /// Key identifier to revoke.
+ /// Revocation timestamp.
+ /// Cancellation token.
+ /// , always.
public Task RevokeAsync(string keyId, DateTimeOffset whenUtc, CancellationToken ct)
{
return Task.FromResult(false);
}
- ///
+ /// Does nothing; the fake never rotates key secrets.
+ /// Key identifier to rotate.
+ /// New secret hash.
+ /// Cancellation token.
+ /// , always.
public Task RotateAsync(string keyId, byte[] newSecretHash, CancellationToken ct)
{
return Task.FromResult(false);
}
- ///
+ /// Does nothing; the fake never deletes keys.
+ /// Key identifier to delete.
+ /// Cancellation token.
+ /// , always.
public Task DeleteAsync(string keyId, CancellationToken ct)
{
return Task.FromResult(false);
@@ -664,24 +683,16 @@ public sealed class DashboardSnapshotServiceTests
int? processId,
WorkerClientState state) : IWorkerClient
{
- ///
- /// Gets the session identifier.
- ///
+ ///
public string SessionId { get; } = sessionId;
- ///
- /// Gets the process identifier.
- ///
+ ///
public int? ProcessId { get; } = processId;
- ///
- /// Gets the current worker client state.
- ///
+ ///
public WorkerClientState State { get; private set; } = state;
- ///
- /// Gets the timestamp of the last heartbeat.
- ///
+ ///
public DateTimeOffset LastHeartbeatAt { get; } = DateTimeOffset.Parse("2026-04-26T10:02:00Z", CultureInfo.InvariantCulture);
///
@@ -699,24 +710,14 @@ public sealed class DashboardSnapshotServiceTests
///
public int KillCount { get; private set; }
- ///
- /// Starts the worker client asynchronously.
- ///
- /// Cancellation token.
- /// Completed task.
+ ///
public Task StartAsync(CancellationToken cancellationToken)
{
StartCount++;
return Task.CompletedTask;
}
- ///
- /// Invokes a worker command asynchronously.
- ///
- /// The command to invoke.
- /// Command timeout.
- /// Cancellation token.
- /// Command reply.
+ ///
public Task InvokeAsync(
WorkerCommand command,
TimeSpan timeout,
@@ -725,11 +726,7 @@ public sealed class DashboardSnapshotServiceTests
return Task.FromResult(new WorkerCommandReply());
}
- ///
- /// Reads events from the worker asynchronously.
- ///
- /// Cancellation token.
- /// Async enumerable of worker events.
+ ///
public async IAsyncEnumerable ReadEventsAsync(
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
{
@@ -737,12 +734,7 @@ public sealed class DashboardSnapshotServiceTests
yield break;
}
- ///
- /// Shuts down the worker client asynchronously.
- ///
- /// Shutdown timeout.
- /// Cancellation token.
- /// Completed task.
+ ///
public Task ShutdownAsync(
TimeSpan timeout,
CancellationToken cancellationToken)
@@ -752,10 +744,7 @@ public sealed class DashboardSnapshotServiceTests
return Task.CompletedTask;
}
- ///
- /// Terminates the worker client.
- ///
- /// Reason for termination.
+ ///
public void Kill(string reason)
{
KillCount++;
diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/HubTokenServiceTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/HubTokenServiceTests.cs
index 943aaad..8ad4f87 100644
--- a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/HubTokenServiceTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/HubTokenServiceTests.cs
@@ -7,7 +7,7 @@ namespace ZB.MOM.WW.MxGateway.Tests.Gateway.Dashboard;
public sealed class HubTokenServiceTests
{
///
- /// Server-039: a token whose data-protected payload has both
+ /// A token whose data-protected payload has both
/// Name and NameIdentifier null (the principal that
/// minted the token had no identity claims) must be rejected by
/// . The role claims alone are
@@ -38,7 +38,7 @@ public sealed class HubTokenServiceTests
///
/// Sanity check: a token minted from a principal with a Name claim
/// validates and returns a principal carrying that identity. Pins
- /// that the Server-039 fix does not over-reject valid tokens.
+ /// that the null-identity rejection above does not over-reject valid tokens.
///
[Fact]
public void Validate_TokenWithName_ReturnsAuthenticatedPrincipal()
diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/GatewayApplicationTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/GatewayApplicationTests.cs
index 5ae6cab..f5825cd 100644
--- a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/GatewayApplicationTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/GatewayApplicationTests.cs
@@ -15,6 +15,7 @@ namespace ZB.MOM.WW.MxGateway.Tests.Gateway;
public sealed class GatewayApplicationTests
{
/// Verifies that Build maps the canonical three health tiers.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task Build_MapsCanonicalHealthEndpoints()
{
@@ -43,12 +44,13 @@ public sealed class GatewayApplicationTests
///
/// Verifies that Build registers the gateway's browse-scope provider so it wins over the
- /// shared library's no-op default (Server-060). The per-API-key browse-subtree scoping
+ /// shared library's no-op default. The per-API-key browse-subtree scoping
/// depends on AddSingleton<IGalaxyBrowseScopeProvider, GatewayBrowseScopeProvider>
/// running before AddZbGalaxyRepository, whose TryAddSingleton default
/// (NullGalaxyBrowseScopeProvider → full hierarchy) must lose. If this registration order ever
/// regressed, every metadata-scoped key would silently see the entire Galaxy hierarchy.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task Build_RegistersGatewayBrowseScopeProviderOverLibraryDefault()
{
@@ -61,6 +63,7 @@ public sealed class GatewayApplicationTests
}
/// Verifies that Build registers the gateway metrics service.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task Build_RegistersGatewayMetrics()
{
@@ -72,6 +75,7 @@ public sealed class GatewayApplicationTests
}
/// Verifies that Build mounts the Prometheus /metrics scrape endpoint.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task Build_MapsMetricsEndpoint()
{
@@ -94,6 +98,7 @@ public sealed class GatewayApplicationTests
}
/// Verifies that Build maps dashboard and authentication endpoints when the dashboard is enabled.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task Build_WhenDashboardEnabled_MapsBlazorDashboardAndAuthEndpoints()
{
@@ -123,6 +128,7 @@ public sealed class GatewayApplicationTests
}
/// Verifies that the dashboard login, logout, and denied endpoints allow anonymous access.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task Build_WhenDashboardEnabled_AuthEndpointsAllowAnonymousAccess()
{
@@ -152,6 +158,7 @@ public sealed class GatewayApplicationTests
}
/// Verifies that dashboard Razor component routes require the dashboard viewer policy.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task Build_WhenDashboardEnabled_ComponentRoutesRequireAuthorization()
{
@@ -180,6 +187,7 @@ public sealed class GatewayApplicationTests
}
/// Verifies that dashboard routes are registered at root when enabled.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task Build_WhenDashboardEnabled_RegistersDashboardRoutesAtRoot()
{
@@ -205,6 +213,7 @@ public sealed class GatewayApplicationTests
}
/// Verifies that dashboard routes are not mapped when disabled.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task Build_WhenDashboardDisabled_DoesNotMapDashboardRoutes()
{
@@ -221,6 +230,7 @@ public sealed class GatewayApplicationTests
/// Configuration key to override.
/// Invalid configuration value.
/// Expected validation error message.
+ /// A task that represents the asynchronous operation.
[Theory]
[InlineData(
"MxGateway:Worker:ExecutablePath",
diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/GatewayEndToEndFakeWorkerSmokeTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/GatewayEndToEndFakeWorkerSmokeTests.cs
index 39ddae7..d9aec6e 100644
--- a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/GatewayEndToEndFakeWorkerSmokeTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/GatewayEndToEndFakeWorkerSmokeTests.cs
@@ -25,6 +25,7 @@ public sealed class GatewayEndToEndFakeWorkerSmokeTests
///
/// Verifies gateway session lifecycle with a scripted fake worker: open, command, event, close.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task GatewayService_WithFakeWorker_CompletesSessionCommandEventAndClosePath()
{
@@ -99,6 +100,7 @@ public sealed class GatewayEndToEndFakeWorkerSmokeTests
/// through the full gRPC→WorkerClient→pipe roundtrip when the fake worker responds
/// with canned replies via RespondToControlCommandAsync.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task GatewayService_WithFakeWorker_ControlCommandsRoundtripThroughGateway()
{
@@ -296,6 +298,7 @@ public sealed class GatewayEndToEndFakeWorkerSmokeTests
///
/// Disposes all active sessions and metrics.
///
+ /// A task that represents the asynchronous operation.
public async ValueTask DisposeAsync()
{
foreach (GatewaySession session in _registry.Snapshot())
@@ -351,12 +354,7 @@ public sealed class GatewayEndToEndFakeWorkerSmokeTests
///
public Task WorkerTask { get; private set; } = Task.CompletedTask;
- ///
- /// Launches a new worker process and returns a handle to manage it.
- ///
- /// Worker process launch request parameters.
- /// Cancellation token.
- /// Worker process handle.
+ ///
public Task LaunchAsync(
WorkerProcessLaunchRequest request,
CancellationToken cancellationToken = default)
@@ -482,6 +480,7 @@ public sealed class GatewayEndToEndFakeWorkerSmokeTests
/// Waits until the scripted worker has responded to one control command.
/// Maximum time to wait.
+ /// A task that represents the asynchronous operation.
public async Task WaitForNextControlCommandAsync(TimeSpan timeout)
{
using CancellationTokenSource cts = new(timeout);
diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/GatewayEndToEndMultiSubscriberTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/GatewayEndToEndMultiSubscriberTests.cs
index dd91232..276bccb 100644
--- a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/GatewayEndToEndMultiSubscriberTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/GatewayEndToEndMultiSubscriberTests.cs
@@ -32,6 +32,7 @@ public sealed class GatewayEndToEndMultiSubscriberTests
/// Two concurrent StreamEvents RPCs on one session both receive every worker event
/// the fake worker emits, in order.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task StreamEvents_TwoSubscribers_BothReceiveAllEvents()
{
@@ -130,6 +131,7 @@ public sealed class GatewayEndToEndMultiSubscriberTests
/// When one of two subscribers cancels its stream, the other subscriber continues
/// to receive subsequent events and the session remains usable.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task StreamEvents_OneSubscriberCancels_OtherContinuesReceivingEvents()
{
@@ -241,6 +243,7 @@ public sealed class GatewayEndToEndMultiSubscriberTests
/// rejected with gRPC status while the first
/// two subscribers keep streaming.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task StreamEvents_ThirdSubscriberWhenCapIsTwo_ReceivesResourceExhausted()
{
@@ -411,6 +414,9 @@ public sealed class GatewayEndToEndMultiSubscriberTests
private readonly GatewayMetrics _metrics = new();
private readonly SessionRegistry _registry = new();
+ /// Initializes a new instance of the class.
+ /// Fake worker process launcher backing the session manager.
+ /// Maximum concurrent event subscribers allowed per session.
public MultiSubscriberGatewayServiceFixture(
IWorkerProcessLauncher launcher,
int maxEventSubscribersPerSession = 8)
@@ -446,6 +452,7 @@ public sealed class GatewayEndToEndMultiSubscriberTests
new FakeGatewayAlarmService());
}
+ /// Gets the gateway service under test.
public MxAccessGatewayService Service { get; }
///
@@ -454,6 +461,10 @@ public sealed class GatewayEndToEndMultiSubscriberTests
/// bounded by . Fails the test if the count is not reached
/// within the deadline.
///
+ /// Identifier of the session to poll.
+ /// Target subscriber count to wait for.
+ /// Maximum time to wait before failing the test.
+ /// A task that represents the asynchronous operation.
///
/// This is the deterministic gate that replaces Task.Delay before Advise
/// calls: it proves the production code has registered each subscriber before we
@@ -485,6 +496,8 @@ public sealed class GatewayEndToEndMultiSubscriberTests
}
}
+ /// Disposes every session in the registry and releases the fixture's metrics.
+ /// A task that represents the asynchronous operation.
public async ValueTask DisposeAsync()
{
foreach (GatewaySession session in _registry.Snapshot())
@@ -533,8 +546,10 @@ public sealed class GatewayEndToEndMultiSubscriberTests
private readonly FakeWorkerProcess _process = new(ProcessId);
+ /// Gets the task representing the fake worker's running background loop.
public Task WorkerTask { get; private set; } = Task.CompletedTask;
+ ///
public Task LaunchAsync(
WorkerProcessLaunchRequest request,
CancellationToken cancellationToken = default)
@@ -625,6 +640,7 @@ public sealed class GatewayEndToEndMultiSubscriberTests
private readonly SemaphoreSlim _emitGate = new(0, 64);
private volatile bool _stopEmitting;
+ /// Gets the task representing the fake worker's running background loop.
public Task WorkerTask { get; private set; } = Task.CompletedTask;
/// Releases the gate so the worker emits one event.
@@ -640,6 +656,7 @@ public sealed class GatewayEndToEndMultiSubscriberTests
_emitGate.Release(); // unblock a pending gate wait if any
}
+ ///
public Task LaunchAsync(
WorkerProcessLaunchRequest request,
CancellationToken cancellationToken = default)
diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/GatewayTlsBootstrapTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/GatewayTlsBootstrapTests.cs
index f34d698..38cc3c9 100644
--- a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/GatewayTlsBootstrapTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/GatewayTlsBootstrapTests.cs
@@ -21,6 +21,7 @@ public sealed class GatewayTlsBootstrapTests
/// trusted dev cert, Kestrel would otherwise serve that dev cert (CN=localhost), so the
/// subject assertion is what makes this test fail without the wiring on either kind of host.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task Host_ServesGeneratedCertificate_WhenHttpsEndpointHasNoCertificate()
{
diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Grpc/EventStreamServiceTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Grpc/EventStreamServiceTests.cs
index a44f36e..01e9b81 100644
--- a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Grpc/EventStreamServiceTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Grpc/EventStreamServiceTests.cs
@@ -17,6 +17,7 @@ public sealed class EventStreamServiceTests
private static readonly TimeSpan TestTimeout = TimeSpan.FromSeconds(5);
/// Verifies that events from the worker stream maintain their original sequence order.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task StreamEventsAsync_YieldsEventsInWorkerOrder()
{
@@ -38,6 +39,7 @@ public sealed class EventStreamServiceTests
}
/// Verifies that a second event subscriber is rejected when one is already active.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task StreamEventsAsync_WhenSecondSubscriberStarts_RejectsClearly()
{
@@ -67,6 +69,7 @@ public sealed class EventStreamServiceTests
}
/// Verifies that canceling an event stream detaches the subscriber cleanly.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task StreamEventsAsync_WhenCanceled_DetachesSubscriber()
{
@@ -89,6 +92,7 @@ public sealed class EventStreamServiceTests
}
/// Verifies that disposing an event stream with buffered events resets the queue depth metric.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task StreamEventsAsync_WhenDisposedWithBufferedEvents_ResetsStreamQueueDepth()
{
@@ -116,6 +120,7 @@ public sealed class EventStreamServiceTests
}
/// Verifies that queue depth metrics correctly track concurrent event streams across multiple sessions.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task StreamEventsAsync_WithConcurrentStreams_TracksAggregateQueueDepth()
{
@@ -157,7 +162,7 @@ public sealed class EventStreamServiceTests
}
///
- /// Re-targeted in Task 5: a per-subscriber channel overflow in the session's
+ /// A per-subscriber channel overflow in the session's
/// faults the whole session under the legacy
/// single-subscriber FailFast policy (the default, single-subscriber mode) and records
/// the overflow + fault metrics. The distributor completes this subscriber's channel
@@ -165,6 +170,7 @@ public sealed class EventStreamServiceTests
/// the pre-epic per-RPC
/// overflow produced.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task StreamEventsAsync_WhenStreamQueueOverflows_FaultsSessionAndReportsOverflow()
{
@@ -189,7 +195,7 @@ public sealed class EventStreamServiceTests
.StreamEventsAsync(CreateRequest(session.SessionId), CancellationToken.None)
.GetAsyncEnumerator();
- // The pump fans 50 events into a capacity-1 subscriber channel faster than this
+ // The pump fans 50 events into a subscriber channel with capacity 1 faster than this
// single reader drains, so one of the reads observes the terminal overflow fault.
SessionManagerException exception = await Assert.ThrowsAsync(
async () =>
@@ -211,12 +217,13 @@ public sealed class EventStreamServiceTests
}
///
- /// Re-targeted in Task 5: under the DisconnectSubscriber policy a per-subscriber
- /// channel overflow disconnects only that subscriber's stream (terminal
+ /// Under the DisconnectSubscriber policy a per-subscriber channel overflow
+ /// disconnects only that subscriber's stream (terminal
/// ) and records the overflow
/// metric, but leaves the session and records no
/// fault. The session, pump, and any other subscribers are unaffected.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task StreamEventsAsync_WhenStreamQueueOverflowsWithDisconnectPolicy_LeavesSessionReady()
{
@@ -259,6 +266,7 @@ public sealed class EventStreamServiceTests
}
/// Verifies that the event stream does not synthesize OperationComplete events from write completions.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task StreamEventsAsync_DoesNotSynthesizeOperationComplete()
{
@@ -276,6 +284,7 @@ public sealed class EventStreamServiceTests
}
/// Verifies that a terminal fault from the worker event stream propagates and faults the session.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task StreamEventsAsync_WhenWorkerEventStreamFaults_PropagatesTerminalFault()
{
@@ -301,9 +310,10 @@ public sealed class EventStreamServiceTests
}
///
- /// Task 12: resuming with AfterWorkerSequence inside the retained window replays exactly
+ /// Resuming with AfterWorkerSequence inside the retained window replays exactly
/// the newer retained events (in order, no dup) then live, with NO ReplayGap sentinel.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task StreamEventsAsync_ResumeWithinRetainedWindow_ReplaysNewerThenLive_NoSentinel()
{
@@ -340,9 +350,10 @@ public sealed class EventStreamServiceTests
}
///
- /// Task 12: resuming with AfterWorkerSequence older than the oldest retained yields the
+ /// Resuming with AfterWorkerSequence older than the oldest retained yields the
/// ReplayGap sentinel FIRST (correct requested/oldest), then the retained tail, then live.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task StreamEventsAsync_ResumeOlderThanOldestRetained_EmitsSentinelFirst_ThenTailThenLive()
{
@@ -385,9 +396,10 @@ public sealed class EventStreamServiceTests
}
///
- /// Task 12: the replay→live boundary is contiguous — no duplicate and no skip — even
+ /// The replay→live boundary is contiguous — no duplicate and no skip — even
/// when events span the handoff.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task StreamEventsAsync_ResumeHandoff_IsContiguous_NoDuplicateNoSkip()
{
@@ -424,9 +436,10 @@ public sealed class EventStreamServiceTests
}
///
- /// Task 12: the per-item filter applies to REPLAYED events identically to live — a
+ /// The per-item filter applies to REPLAYED events identically to live — a
/// replayed event at/below the requested watermark is never delivered.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task StreamEventsAsync_ResumeReplay_AppliesPerItemFilter_DropsAtOrBelowWatermark()
{
@@ -466,9 +479,10 @@ public sealed class EventStreamServiceTests
}
///
- /// Task 12: AfterWorkerSequence == 0 is a fresh stream (not a resume) — no replay, no
+ /// AfterWorkerSequence == 0 is a fresh stream (not a resume) — no replay, no
/// sentinel, just live events as before.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task StreamEventsAsync_FreshStreamAfterSequenceZero_NoReplayNoSentinel()
{
@@ -816,7 +830,8 @@ public sealed class EventStreamServiceTests
State = WorkerClientState.Faulted;
}
- ///
+ /// No-op disposal; the fake holds no unmanaged resources.
+ /// A task that represents the asynchronous operation.
public ValueTask DisposeAsync()
{
return ValueTask.CompletedTask;
diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Grpc/GalaxyRepositoryHostWiringTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Grpc/GalaxyRepositoryHostWiringTests.cs
index 4a7b429..2be85ae 100644
--- a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Grpc/GalaxyRepositoryHostWiringTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Grpc/GalaxyRepositoryHostWiringTests.cs
@@ -29,6 +29,7 @@ public sealed class GalaxyRepositoryHostWiringTests
/// threads the constraint into the lib
/// service → BrowseChildren returns empty children.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task BrowseChildren_BrowseSubtreesConstraintThroughHostWiring_FiltersChildren()
{
@@ -130,13 +131,17 @@ public sealed class GalaxyRepositoryHostWiringTests
/// Immediately-ready stub: Current returns the seeded entry, loads are instant.
private sealed class StubGalaxyHierarchyCache(GalaxyHierarchyCacheEntry current) : IGalaxyHierarchyCache
{
- ///
+ /// Gets the seeded cache entry passed to the stub's constructor.
public GalaxyHierarchyCacheEntry Current { get; } = current;
- ///
+ /// No-op: the stub's entry is always considered already loaded.
+ /// Cancellation token.
+ /// A task that represents the asynchronous operation.
public Task RefreshAsync(CancellationToken cancellationToken) => Task.CompletedTask;
- ///
+ /// No-op: the stub's entry is always considered already loaded.
+ /// Cancellation token.
+ /// A task that represents the asynchronous operation.
public Task WaitForFirstLoadAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
@@ -146,23 +151,33 @@ public sealed class GalaxyRepositoryHostWiringTests
///
private sealed class StubGalaxyRepository : IGalaxyRepository
{
- ///
+ /// Always throws: BrowseChildren never calls the repository directly.
+ /// Cancellation token.
+ /// Never returns; always throws.
public Task TestConnectionAsync(CancellationToken ct = default) =>
throw new NotSupportedException("Not called during BrowseChildren.");
- ///
+ /// Always throws: BrowseChildren never calls the repository directly.
+ /// Cancellation token.
+ /// Never returns; always throws.
public Task GetLastDeployTimeAsync(CancellationToken ct = default) =>
throw new NotSupportedException("Not called during BrowseChildren.");
- ///
+ /// Always throws: BrowseChildren never calls the repository directly.
+ /// Cancellation token.
+ /// Never returns; always throws.
public Task> GetHierarchyAsync(CancellationToken ct = default) =>
throw new NotSupportedException("Not called during BrowseChildren.");
- ///
+ /// Always throws: BrowseChildren never calls the repository directly.
+ /// Cancellation token.
+ /// Never returns; always throws.
public Task> GetAttributesAsync(CancellationToken ct = default) =>
throw new NotSupportedException("Not called during BrowseChildren.");
- ///
+ /// Always throws: BrowseChildren never calls the repository directly.
+ /// Cancellation token.
+ /// Never returns; always throws.
public Task> GetAlarmAttributesAsync(CancellationToken ct = default) =>
throw new NotSupportedException("Not called during BrowseChildren.");
}
diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Grpc/MxAccessGatewayServiceConstraintTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Grpc/MxAccessGatewayServiceConstraintTests.cs
index 0ebdc72..24df533 100644
--- a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Grpc/MxAccessGatewayServiceConstraintTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Grpc/MxAccessGatewayServiceConstraintTests.cs
@@ -13,7 +13,7 @@ using ZB.MOM.WW.MxGateway.Tests.TestSupport;
namespace ZB.MOM.WW.MxGateway.Tests.Gateway.Grpc;
///
-/// Tests for Server-021. MxAccessGatewayService.ApplyConstraintsAsync and
+/// MxAccessGatewayService.ApplyConstraintsAsync and
/// the BulkConstraintPlan / ReadBulkConstraintPlan /
/// WriteBulkConstraintPlan / SubscribeBulkConstraintPlan reply-merge
/// logic was previously exercised only with an allow-all enforcer, so denial
@@ -34,6 +34,7 @@ public sealed class MxAccessGatewayServiceConstraintTests
/// worker once with only the allowed tags, then splice the denied entries
/// back into the reply at their original indices.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task Invoke_AddItemBulk_WithMixedDenials_InterleavesDeniedAndAllowedInOriginalIndexOrder()
{
@@ -95,6 +96,7 @@ public sealed class MxAccessGatewayServiceConstraintTests
/// false, return the
/// denied-only reply, and never call the session manager.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task Invoke_SubscribeBulk_WhenAllTagsDenied_DoesNotCallWorkerAndReturnsDeniedReply()
{
@@ -118,6 +120,7 @@ public sealed class MxAccessGatewayServiceConstraintTests
/// FilterHandleBulkAsync against CheckReadHandleAsync. Partial
/// denial must still produce a merged-by-index BulkSubscribeReply.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task Invoke_AdviseItemBulk_WithMixedHandleDenials_MergesDeniedIntoReply()
{
@@ -166,6 +169,7 @@ public sealed class MxAccessGatewayServiceConstraintTests
/// unchanged — the constraint plan is null and no merge occurs. Regression
/// guard against accidentally engaging the merge path for the common case.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task Invoke_SubscribeBulk_WithAllowAllEnforcer_PassesThroughUnchanged()
{
@@ -208,6 +212,7 @@ public sealed class MxAccessGatewayServiceConstraintTests
/// the SubscribeBulk family because the reply slot is
/// BulkReadReply, not BulkSubscribeReply.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task Invoke_ReadBulk_WithMixedDenials_MergesDeniedBulkReadResults()
{
@@ -255,6 +260,7 @@ public sealed class MxAccessGatewayServiceConstraintTests
/// 's ReadBulkConstraintPlan
/// CreateDeniedReply path.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task Invoke_ReadBulk_WhenAllTagsDenied_ShortCircuitsWithDeniedOnlyReply()
{
@@ -279,6 +285,7 @@ public sealed class MxAccessGatewayServiceConstraintTests
/// forwarded command and splice a denied BulkWriteResult back in at
/// the original index.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task Invoke_WriteBulk_WithDeniedHandle_DropsEntryFromWorkerCallAndMergesDenialIntoReply()
{
@@ -329,6 +336,7 @@ public sealed class MxAccessGatewayServiceConstraintTests
/// switch arm than plain WriteBulk. The merge logic is shared, so a
/// full denial here is enough to prove the secured-bulk routing.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task Invoke_WriteSecuredBulk_WhenAllHandlesDenied_ShortCircuitsWithDeniedOnlyReply()
{
@@ -347,13 +355,14 @@ public sealed class MxAccessGatewayServiceConstraintTests
}
///
- /// Tests-020: Write2Bulk takes the third GetPayload/SetPayload
+ /// Write2Bulk takes the third GetPayload/SetPayload
/// switch arm in WriteBulkConstraintPlan. The merge logic is shared with
/// WriteBulk, but a full denial through the CreateDeniedReply path
/// proves the Write2Bulk arm of the per-kind SetPayload switch fires
/// (and not, say, WriteBulk by mistake) — guarding against a refactor that
/// drops or misroutes the Write2Bulk case.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task Invoke_Write2Bulk_WhenAllHandlesDenied_ShortCircuitsWithDeniedOnlyReply()
{
@@ -377,11 +386,12 @@ public sealed class MxAccessGatewayServiceConstraintTests
}
///
- /// Tests-020: WriteSecured2Bulk takes the fourth GetPayload/SetPayload
+ /// WriteSecured2Bulk takes the fourth GetPayload/SetPayload
/// switch arm in WriteBulkConstraintPlan. Same reasoning as
/// Write2Bulk — assert the WriteSecured2Bulk reply slot is populated
/// to prove that arm of the switch fires.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task Invoke_WriteSecured2Bulk_WhenAllHandlesDenied_ShortCircuitsWithDeniedOnlyReply()
{
@@ -404,10 +414,10 @@ public sealed class MxAccessGatewayServiceConstraintTests
Assert.Empty(reply.WriteSecuredBulk?.Results ?? new Google.Protobuf.Collections.RepeatedField());
}
- // === Worker reply-count divergence (Tests-024) ===
+ // === Worker reply-count divergence ===
///
- /// Tests-024: WriteBulkConstraintPlan.MergeDeniedInto dequeues from
+ /// WriteBulkConstraintPlan.MergeDeniedInto dequeues from
/// allowedResults per non-denied slot via Queue.TryDequeue,
/// which silently returns false when the queue is empty. Pin the
/// observable behaviour when the worker returns FEWER allowed results than
@@ -417,6 +427,7 @@ public sealed class MxAccessGatewayServiceConstraintTests
/// This fixture makes that "silent truncate" behaviour explicit so a future
/// change either fills the gap with a synthetic failure or fails this test.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task Invoke_WriteBulk_WhenWorkerReturnsFewerResultsThanAllowed_MergedReplyIsTruncated()
{
@@ -464,12 +475,13 @@ public sealed class MxAccessGatewayServiceConstraintTests
}
///
- /// Tests-024: when the worker returns MORE allowed results than the
+ /// When the worker returns MORE allowed results than the
/// gateway forwarded, the extras must be silently ignored — the merged
/// reply length stays at OriginalCount. This pins the
/// for index < OriginalCount loop bound so a regression that
/// accidentally surfaces extras as trailing results is caught.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task Invoke_WriteBulk_WhenWorkerReturnsExtraResults_IgnoresExtras()
{
@@ -527,6 +539,7 @@ public sealed class MxAccessGatewayServiceConstraintTests
/// via EnforceWriteHandleAsync
/// and never reach the session manager.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task Invoke_Write_WithDeniedHandle_ThrowsPermissionDeniedAndDoesNotCallWorker()
{
@@ -554,6 +567,7 @@ public sealed class MxAccessGatewayServiceConstraintTests
/// enforce helper -> RecordDenialAsync, so the recorded denial carries the exact
/// id the client sent (including non-GUID trace ids used by Rust/Python/Java clients).
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task Invoke_Write_WithDeniedHandle_ThreadsClientCorrelationIdIntoRecordedDenial()
{
@@ -581,6 +595,7 @@ public sealed class MxAccessGatewayServiceConstraintTests
/// ApplyConstraintsAsync (Write/Write2/WriteSecured/WriteSecured2) is
/// reachable for at least one of the secured kinds.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task Invoke_WriteSecured_WithDeniedHandle_ThrowsPermissionDenied()
{
@@ -604,6 +619,7 @@ public sealed class MxAccessGatewayServiceConstraintTests
/// via EnforceReadTagAsync
/// and never reach the session manager.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task Invoke_AddItem_WithDeniedTag_ThrowsPermissionDeniedAndDoesNotCallWorker()
{
@@ -881,11 +897,7 @@ public sealed class MxAccessGatewayServiceConstraintTests
/// The session to seed.
public void SeedSession(GatewaySession session) => seededSessions[session.SessionId] = session;
- /// Opens a test session asynchronously.
- /// The session open request.
- /// The client identity, if any.
- /// The API key identifier of the caller, if any.
- /// Token to observe for cancellation.
+ ///
public Task OpenSessionAsync(
SessionOpenRequest request,
string? clientIdentity,
@@ -893,9 +905,7 @@ public sealed class MxAccessGatewayServiceConstraintTests
CancellationToken cancellationToken) =>
Task.FromResult(seededSessions.Values.First());
- /// Tries to get a test session by identifier.
- /// The session identifier.
- /// The session, if found.
+ ///
public bool TryGetSession(string sessionId, out GatewaySession session)
{
if (seededSessions.TryGetValue(sessionId, out GatewaySession? seeded))
@@ -914,10 +924,7 @@ public sealed class MxAccessGatewayServiceConstraintTests
return true;
}
- /// Invokes a worker command and returns the reply asynchronously.
- /// The session identifier.
- /// The worker command.
- /// Token to observe for cancellation.
+ ///
public Task InvokeAsync(
string sessionId,
WorkerCommand command,
@@ -928,9 +935,7 @@ public sealed class MxAccessGatewayServiceConstraintTests
return Task.FromResult(InvokeReply);
}
- /// Reads events from the session asynchronously.
- /// The session identifier.
- /// Token to observe for cancellation.
+ ///
public async IAsyncEnumerable ReadEventsAsync(
string sessionId,
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
@@ -943,33 +948,25 @@ public sealed class MxAccessGatewayServiceConstraintTests
}
}
- /// Closes a test session asynchronously.
- /// The session identifier.
- /// Token to observe for cancellation.
+ ///
public Task CloseSessionAsync(
string sessionId,
CancellationToken cancellationToken) =>
Task.FromResult(new SessionCloseResult(sessionId, SessionState.Closed, AlreadyClosed: false));
- /// Kills a worker process asynchronously.
- /// The session identifier.
- /// The reason for killing the worker.
- /// Token to observe for cancellation.
+ ///
public Task KillWorkerAsync(
string sessionId,
string reason,
CancellationToken cancellationToken) =>
Task.FromResult(new SessionCloseResult(sessionId, SessionState.Closed, AlreadyClosed: false));
- /// Closes expired session leases asynchronously.
- /// The current time to check against.
- /// Token to observe for cancellation.
+ ///
public Task CloseExpiredLeasesAsync(
DateTimeOffset now,
CancellationToken cancellationToken) => Task.FromResult(0);
- /// Shuts down the test session manager asynchronously.
- /// Token to observe for cancellation.
+ ///
public Task ShutdownAsync(CancellationToken cancellationToken) => Task.CompletedTask;
private static GatewaySession CreateFallbackSession(string sessionId)
@@ -994,9 +991,7 @@ public sealed class MxAccessGatewayServiceConstraintTests
private sealed class FakeEventStreamService(FakeSessionManager sessionManager) : IEventStreamService
{
- /// Streams events for the test session asynchronously.
- /// The stream events request.
- /// Token to observe for cancellation.
+ ///
public async IAsyncEnumerable StreamEventsAsync(
StreamEventsRequest request,
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
@@ -1012,33 +1007,28 @@ public sealed class MxAccessGatewayServiceConstraintTests
private sealed class FakeWorkerClient : IWorkerClient
{
- /// Gets the test session identifier.
+ ///
public string SessionId { get; } = MxAccessGatewayServiceConstraintTests.SessionId;
- /// Gets the test worker process identifier.
+ ///
public int? ProcessId { get; } = 1234;
- /// Gets the test worker client state.
+ ///
public WorkerClientState State { get; } = WorkerClientState.Ready;
- /// Gets the last recorded heartbeat time.
+ ///
public DateTimeOffset LastHeartbeatAt { get; } = DateTimeOffset.UtcNow;
- /// Starts the test worker client asynchronously.
- /// Token to observe for cancellation.
+ ///
public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
- /// Invokes a command on the test worker asynchronously.
- /// The worker command.
- /// Maximum time to wait for completion.
- /// Token to observe for cancellation.
+ ///
public Task InvokeAsync(
WorkerCommand command,
TimeSpan timeout,
CancellationToken cancellationToken) => Task.FromResult(new WorkerCommandReply());
- /// Reads events from the test worker asynchronously.
- /// Token to observe for cancellation.
+ ///
public async IAsyncEnumerable ReadEventsAsync(
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
{
@@ -1046,18 +1036,16 @@ public sealed class MxAccessGatewayServiceConstraintTests
yield break;
}
- /// Shuts down the test worker client asynchronously.
- /// Maximum time to wait for completion.
- /// Token to observe for cancellation.
+ ///
public Task ShutdownAsync(TimeSpan timeout, CancellationToken cancellationToken) => Task.CompletedTask;
- /// Kills the test worker process.
- /// The reason for killing the worker.
+ ///
public void Kill(string reason)
{
}
- ///
+ /// Disposes the test worker client. No-op — there is no unmanaged state to release.
+ /// A task that represents the asynchronous operation.
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
}
}
diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Grpc/MxAccessGatewayServiceTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Grpc/MxAccessGatewayServiceTests.cs
index 5ddc009..de76a20 100644
--- a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Grpc/MxAccessGatewayServiceTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Grpc/MxAccessGatewayServiceTests.cs
@@ -18,6 +18,7 @@ namespace ZB.MOM.WW.MxGateway.Tests.Gateway.Grpc;
public sealed class MxAccessGatewayServiceTests
{
/// Verifies that OpenSession returns correct session details for a valid request.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task OpenSession_WithValidRequest_ReturnsSessionDetails()
{
@@ -56,6 +57,7 @@ public sealed class MxAccessGatewayServiceTests
/// TryGetSession return false, so this test fails if the service drops
/// its missing-session check rather than passing for the wrong reason.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task Invoke_WhenSessionMissing_ThrowsNotFound()
{
@@ -78,6 +80,7 @@ public sealed class MxAccessGatewayServiceTests
/// manager when is on,
/// confirming the missing-session test above is gated on a real lookup.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task Invoke_WhenSessionSeeded_ResolvesAndInvokes()
{
@@ -94,6 +97,7 @@ public sealed class MxAccessGatewayServiceTests
}
/// Verifies that Invoke throws InvalidArgument and does not invoke the session manager when payload is mismatched.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task Invoke_WithMismatchedPayload_ThrowsInvalidArgumentAndDoesNotCallSessionManager()
{
@@ -117,6 +121,7 @@ public sealed class MxAccessGatewayServiceTests
}
/// Verifies that Invoke returns HResult status and method payload from worker reply.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task Invoke_WithWorkerReply_ReturnsHresultStatusAndMethodPayload()
{
@@ -172,6 +177,7 @@ public sealed class MxAccessGatewayServiceTests
}
/// Verifies that StreamEvents writes only events after the specified worker sequence.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task StreamEvents_WithAfterSequence_WritesOnlyLaterEvents()
{
@@ -197,15 +203,8 @@ public sealed class MxAccessGatewayServiceTests
///
/// Verifies that StreamEvents records the send-duration histogram per event.
- ///
- /// Tests-027 (concurrency flake): the listener must filter by the specific
- /// instance owned by this test, not by the process-shared
- /// . Otherwise a parallel test that constructs its own
- /// and records mxgateway.events.stream_send.duration would
- /// cross-contaminate families and break the equality assertion below. See the companion
- ///
- /// regression for the cross-talk reproduction.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task StreamEvents_WhenEventIsWritten_RecordsSendDuration()
{
@@ -252,13 +251,14 @@ public sealed class MxAccessGatewayServiceTests
}
///
- /// Tests-027 regression: a that filters by the specific
+ /// A that filters by the specific
/// instance (via )
/// must NOT observe measurements recorded on a different that shares
/// the same . This is the cross-talk vector that previously
/// caused StreamEvents_WhenEventIsWritten_RecordsSendDuration to fail intermittently when
/// run in parallel with another test recording the same histogram.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task StreamEvents_RecordSendDurationListener_IgnoresMeasurementsFromOtherMetersWithSameName()
{
@@ -315,6 +315,7 @@ public sealed class MxAccessGatewayServiceTests
}
/// Verifies that CloseSession throws InvalidArgument when session ID is blank.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task CloseSession_WithBlankSessionId_ThrowsInvalidArgument()
{
@@ -335,6 +336,7 @@ public sealed class MxAccessGatewayServiceTests
// alarm feed. CreateService injects FakeGatewayAlarmService.
/// Verifies AcknowledgeAlarm rejects an empty alarm_full_reference.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task AcknowledgeAlarm_WithMissingAlarmReference_ThrowsInvalidArgument()
{
@@ -349,6 +351,7 @@ public sealed class MxAccessGatewayServiceTests
}
/// Verifies AcknowledgeAlarm delegates a valid request to the alarm service.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task AcknowledgeAlarm_WithValidRequest_DelegatesToAlarmService()
{
@@ -369,6 +372,7 @@ public sealed class MxAccessGatewayServiceTests
}
/// Verifies StreamAlarms forwards the central alarm feed, ending with snapshot_complete.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task StreamAlarms_ForwardsTheCentralAlarmFeed()
{
@@ -386,6 +390,7 @@ public sealed class MxAccessGatewayServiceTests
}
/// Verifies OpenSession advertises the alarm RPC capability strings.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task OpenSession_AdvertisesAlarmRpcCapabilities()
{
@@ -715,7 +720,8 @@ public sealed class MxAccessGatewayServiceTests
{
}
- ///
+ /// Completes immediately without disposing any resources.
+ /// A task that represents the asynchronous operation.
public ValueTask DisposeAsync()
{
return ValueTask.CompletedTask;
diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Sessions/ArrayAddressNormalizerTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Sessions/ArrayAddressNormalizerTests.cs
index d90f2bb..de75fb9 100644
--- a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Sessions/ArrayAddressNormalizerTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Sessions/ArrayAddressNormalizerTests.cs
@@ -43,6 +43,7 @@ public sealed class ArrayAddressNormalizerTests
}
/// Verifies null, empty, and whitespace addresses are returned unchanged.
+ /// The blank address under test.
[Theory]
[InlineData("")]
[InlineData(" ")]
@@ -96,10 +97,14 @@ public sealed class ArrayAddressNormalizerTests
/// Gets the current cache entry.
public GalaxyHierarchyCacheEntry Current { get; } = current;
- ///
+ /// No-op stub refresh; the fixed entry never changes.
+ /// Token to cancel the asynchronous operation.
+ /// A task that represents the asynchronous operation.
public Task RefreshAsync(CancellationToken cancellationToken) => Task.CompletedTask;
- ///
+ /// No-op stub wait; the fixed entry is already loaded.
+ /// Token to cancel the asynchronous operation.
+ /// A task that represents the asynchronous operation.
public Task WaitForFirstLoadAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
}
diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Sessions/GatewayArrayWriteWiringTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Sessions/GatewayArrayWriteWiringTests.cs
index 7991938..8236226 100644
--- a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Sessions/GatewayArrayWriteWiringTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Sessions/GatewayArrayWriteWiringTests.cs
@@ -19,6 +19,7 @@ public sealed class GatewayArrayWriteWiringTests
/// A bare array AddItem address is normalized to its writable array form on the wire,
/// and the normalized address lands in the tracked .
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task AddItem_BareArrayAddress_NormalizedOnWireAndInRegistration()
{
@@ -71,6 +72,7 @@ public sealed class GatewayArrayWriteWiringTests
}
/// A bare scalar AddItem address is forwarded unchanged.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task AddItem_ScalarAddress_ForwardedUnchanged()
{
@@ -99,6 +101,7 @@ public sealed class GatewayArrayWriteWiringTests
/// A sparse-array value is expanded to a full, default-filled
/// before reaching the worker; no sparse value is ever forwarded.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task Write_SparseArrayValue_ExpandedBeforeReachingWorker()
{
@@ -145,6 +148,7 @@ public sealed class GatewayArrayWriteWiringTests
/// A bare array AddItem2 address is normalized to its writable array form on the wire,
/// and the normalized address lands in the tracked .
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task AddItem2_BareArrayAddress_NormalizedOnWireAndInRegistration()
{
@@ -199,6 +203,7 @@ public sealed class GatewayArrayWriteWiringTests
/// default-filled before reaching the worker; no sparse value is ever
/// forwarded inside a bulk write.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task WriteBulk_SparseArrayEntryValue_ExpandedBeforeReachingWorker()
{
@@ -253,6 +258,7 @@ public sealed class GatewayArrayWriteWiringTests
/// same batch is forwarded unchanged. Tracking the worker's echoed reply lands the normalized
/// address in the .
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task AddItemBulk_BareArrayAddress_NormalizedOnWireAndInRegistration()
{
@@ -329,6 +335,7 @@ public sealed class GatewayArrayWriteWiringTests
/// default-filled before reaching the worker; the secured variant's
/// case arm in NormalizeOutboundCommand is wired to the expander.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task WriteSecured_SparseArrayValue_ExpandedBeforeReachingWorker()
{
@@ -376,6 +383,7 @@ public sealed class GatewayArrayWriteWiringTests
/// default-filled before reaching the worker; the Write2 variant's
/// case arm in NormalizeOutboundCommand is wired to the expander.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task Write2_SparseArrayValue_ExpandedBeforeReachingWorker()
{
@@ -424,6 +432,7 @@ public sealed class GatewayArrayWriteWiringTests
/// WriteSecured2 variant's case arm in NormalizeOutboundCommand is wired to
/// the expander.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task WriteSecured2_SparseArrayValue_ExpandedBeforeReachingWorker()
{
@@ -471,6 +480,7 @@ public sealed class GatewayArrayWriteWiringTests
/// full, default-filled before reaching the worker; the Write2Bulk
/// variant's case arm in NormalizeOutboundCommand is wired to the expander.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task Write2Bulk_SparseArrayEntryValue_ExpandedBeforeReachingWorker()
{
@@ -524,6 +534,7 @@ public sealed class GatewayArrayWriteWiringTests
/// default-filled before reaching the worker; the WriteSecuredBulk
/// variant's case arm in NormalizeOutboundCommand is wired to the expander.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task WriteSecuredBulk_SparseArrayEntryValue_ExpandedBeforeReachingWorker()
{
@@ -578,6 +589,7 @@ public sealed class GatewayArrayWriteWiringTests
/// WriteSecured2Bulk variant's case arm in NormalizeOutboundCommand is wired to
/// the expander.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task WriteSecured2Bulk_SparseArrayEntryValue_ExpandedBeforeReachingWorker()
{
@@ -630,6 +642,7 @@ public sealed class GatewayArrayWriteWiringTests
/// A bare array address added via AddBufferedItem is normalized to its writable array
/// form on the wire and in the tracked .
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task AddBufferedItem_BareArrayAddress_NormalizedOnWireAndInRegistration()
{
@@ -745,10 +758,14 @@ public sealed class GatewayArrayWriteWiringTests
/// Gets the current cache entry.
public GalaxyHierarchyCacheEntry Current { get; } = current;
- ///
+ /// No-op for this stub; the fixed entry is always ready.
+ /// Token to observe for cancellation.
+ /// A task that represents the asynchronous operation.
public Task RefreshAsync(CancellationToken cancellationToken) => Task.CompletedTask;
- ///
+ /// No-op for this stub; the fixed entry is always considered loaded.
+ /// Token to observe for cancellation.
+ /// A task that represents the asynchronous operation.
public Task WaitForFirstLoadAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
@@ -760,16 +777,16 @@ public sealed class GatewayArrayWriteWiringTests
/// Gets or sets the reply returned by the next invocation.
public WorkerCommandReply NextReply { get; set; } = new();
- /// Gets the session identifier.
+ ///
public string SessionId { get; } = "session-array-write-wiring";
- /// Gets the worker process identifier.
+ ///
public int? ProcessId { get; } = 1234;
- /// Gets the worker client state.
+ ///
public WorkerClientState State { get; } = WorkerClientState.Ready;
- /// Gets the last recorded heartbeat timestamp.
+ ///
public DateTimeOffset LastHeartbeatAt { get; } = DateTimeOffset.UtcNow;
///
@@ -801,7 +818,8 @@ public sealed class GatewayArrayWriteWiringTests
{
}
- ///
+ /// Disposes the test worker client. No-op — there is no unmanaged state to release.
+ /// A task that represents the asynchronous operation.
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
}
}
diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Sessions/GatewaySessionDashboardMirrorTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Sessions/GatewaySessionDashboardMirrorTests.cs
index 037e12f..b52292e 100644
--- a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Sessions/GatewaySessionDashboardMirrorTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Sessions/GatewaySessionDashboardMirrorTests.cs
@@ -14,7 +14,7 @@ using ZB.MOM.WW.MxGateway.Tests.TestSupport;
namespace ZB.MOM.WW.MxGateway.Tests.Gateway.Sessions;
///
-/// Task 6 regression tests for the internal dashboard mirror. The dashboard is a
+/// Regression tests for the internal dashboard mirror. The dashboard is a
/// first-class subscriber on the session's , so it
/// receives session events whether or not a gRPC client is streaming — fixing the
/// "dark feed" where the dashboard only saw events while a gRPC client was actively
@@ -28,9 +28,8 @@ public sealed class GatewaySessionDashboardMirrorTests
/// The KEY bug-fix test: the dashboard broadcaster receives session events even when
/// NO gRPC StreamEvents subscriber is attached. The session is driven to Ready
/// with a fake worker emitting events; only the internal dashboard subscriber exists.
- /// Before Task 6 the mirror lived inside the per-RPC gRPC loop, so with no gRPC
- /// subscriber the dashboard saw nothing.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task DashboardMirror_ReceivesEvents_WithNoGrpcSubscriber()
{
@@ -59,6 +58,7 @@ public sealed class GatewaySessionDashboardMirrorTests
/// gRPC path is no longer the dashboard's source — both read independent leases fed by
/// the single distributor pump.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task DashboardMirror_AndGrpcSubscriber_BothReceiveEvents()
{
@@ -108,11 +108,12 @@ public sealed class GatewaySessionDashboardMirrorTests
}
///
- /// Task 4 hazard guard: starting the pump at Ready with a fast-completing worker stream
+ /// Hazard guard: starting the pump at Ready with a fast-completing worker stream
/// and zero subscribers used to drain into nothing and leave a later subscriber hanging.
/// Now the dashboard subscriber is registered BEFORE the pump starts, so even a worker
/// stream that completes immediately delivers every event to the dashboard with no hang.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task DashboardMirror_FastCompletingWorkerStream_DeliversAllEventsWithoutHang()
{
@@ -134,6 +135,7 @@ public sealed class GatewaySessionDashboardMirrorTests
/// The dashboard Publish must be never-throw at the seam too: a throwing broadcaster
/// must not fault the session or stop the mirror from continuing past the failure.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task DashboardMirror_WhenBroadcasterThrows_DoesNotFaultSessionAndKeepsMirroring()
{
@@ -155,6 +157,7 @@ public sealed class GatewaySessionDashboardMirrorTests
/// The internal dashboard subscriber must NOT count against the single-subscriber
/// guard: a gRPC subscriber can still attach while the dashboard mirror is running.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task DashboardMirror_DoesNotCountAgainstSingleSubscriberGuard()
{
@@ -239,8 +242,10 @@ public sealed class GatewaySessionDashboardMirrorTests
{
private int _publishAttempts;
+ /// Number of times has been invoked.
public int PublishAttempts => Volatile.Read(ref _publishAttempts);
+ ///
public void Publish(string sessionId, MxEvent mxEvent)
{
Interlocked.Increment(ref _publishAttempts);
@@ -250,49 +255,59 @@ public sealed class GatewaySessionDashboardMirrorTests
private sealed class SingleSessionManager(GatewaySession session) : ISessionManager
{
+ ///
public Task OpenSessionAsync(
SessionOpenRequest request,
string? clientIdentity,
string? ownerKeyId,
CancellationToken cancellationToken) => Task.FromResult(session);
+ ///
public bool TryGetSession(string sessionId, out GatewaySession gatewaySession)
{
gatewaySession = session;
return string.Equals(sessionId, session.SessionId, StringComparison.Ordinal);
}
+ ///
public Task InvokeAsync(
string sessionId,
WorkerCommand command,
CancellationToken cancellationToken) => Task.FromResult(new WorkerCommandReply());
+ ///
public IAsyncEnumerable ReadEventsAsync(
string sessionId,
CancellationToken cancellationToken) => session.ReadEventsAsync(cancellationToken);
+ ///
public Task CloseSessionAsync(
string sessionId,
CancellationToken cancellationToken) =>
Task.FromResult(new SessionCloseResult(sessionId, SessionState.Closed, AlreadyClosed: false));
+ ///
public Task KillWorkerAsync(
string sessionId,
string reason,
CancellationToken cancellationToken) =>
Task.FromResult(new SessionCloseResult(sessionId, SessionState.Closed, AlreadyClosed: false));
+ ///
public Task CloseExpiredLeasesAsync(
DateTimeOffset now,
CancellationToken cancellationToken) => Task.FromResult(0);
+ ///
public Task ShutdownAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
private sealed class FakeWorkerClient : IWorkerClient
{
+ /// Events yielded by , in order.
public List Events { get; } = [];
+ /// When , completes after yielding instead of hanging.
public bool CompleteAfterConfiguredEvents { get; set; }
// Gate that holds the event stream before it yields anything. Released by default, so
@@ -308,26 +323,35 @@ public sealed class GatewaySessionDashboardMirrorTests
return gate;
}
+ /// Re-arms the release gate so blocks before yielding events until is called.
public void HoldEventsUntilReleased() =>
_releaseGate = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ /// Releases the gate set by , letting proceed.
public void ReleaseEvents() => _releaseGate.TrySetResult();
+ ///
public string SessionId { get; } = "session-dashboard-mirror";
+ ///
public int? ProcessId { get; } = 1234;
+ ///
public WorkerClientState State { get; } = WorkerClientState.Ready;
+ ///
public DateTimeOffset LastHeartbeatAt { get; } = DateTimeOffset.UtcNow;
+ ///
public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+ ///
public Task InvokeAsync(
WorkerCommand command,
TimeSpan timeout,
CancellationToken cancellationToken) => Task.FromResult(new WorkerCommandReply());
+ ///
public async IAsyncEnumerable ReadEventsAsync(
[EnumeratorCancellation] CancellationToken cancellationToken)
{
@@ -348,12 +372,16 @@ public sealed class GatewaySessionDashboardMirrorTests
await Task.Delay(Timeout.InfiniteTimeSpan, cancellationToken);
}
+ ///
public Task ShutdownAsync(TimeSpan timeout, CancellationToken cancellationToken) => Task.CompletedTask;
+ ///
public void Kill(string reason)
{
}
+ /// No-op dispose; the fake worker client owns no resources to release.
+ /// A task that represents the asynchronous operation.
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
}
}
diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Sessions/GatewaySessionTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Sessions/GatewaySessionTests.cs
index 27a8947..7dfaef8 100644
--- a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Sessions/GatewaySessionTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Sessions/GatewaySessionTests.cs
@@ -13,21 +13,21 @@ using ZB.MOM.WW.MxGateway.Tests.TestSupport;
namespace ZB.MOM.WW.MxGateway.Tests.Gateway.Sessions;
///
-/// Concurrency and disposal regression tests for .
-/// Server-015 and Server-016 audited the split lock discipline between
-/// _syncRoot (state transitions) and _closeLock (close serialization)
-/// and the un-gated DisposeAsync; these tests pin the post-fix behavior.
+/// Concurrency and disposal regression tests for ,
+/// covering the split lock discipline between _syncRoot (state transitions)
+/// and _closeLock (close serialization) and the gated DisposeAsync.
///
public sealed class GatewaySessionTests
{
///
- /// Server-015 regression. A TransitionTo(Ready) issued after
+ /// A TransitionTo(Ready) issued after
/// has set
/// must not flip the session back to . The
/// blocking worker shutdown keeps CloseAsync parked between the
/// Closing write and the Closed write, which is precisely the
/// window the audit identified.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task TransitionTo_AfterCloseStarted_DoesNotOverwriteClosing()
{
@@ -54,12 +54,13 @@ public sealed class GatewaySessionTests
}
///
- /// Server-015 regression. Once finishes,
+ /// Once finishes,
/// must not be able to move the
/// session out of either — the close path's
/// terminal write goes through the same _syncRoot the rest of the state
/// machine uses, so the existing "Closed is terminal" invariant holds.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task MarkFaulted_AfterCloseCompletes_DoesNotResurrectSession()
{
@@ -76,7 +77,7 @@ public sealed class GatewaySessionTests
}
///
- /// Server-028 regression. A issued
+ /// A issued
/// while is parked between its
/// Closing and Closed writes must not break the close path's
/// terminal contract: the in-flight close runs to Closed, the fault
@@ -86,6 +87,7 @@ public sealed class GatewaySessionTests
/// Faulted" — this test pins the resolved end state so a future tightening
/// of MarkFaulted cannot silently regress it.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task MarkFaulted_DuringInFlightClose_PreservesFaultButYieldsToClose()
{
@@ -113,11 +115,12 @@ public sealed class GatewaySessionTests
}
///
- /// Server-016 regression. must wait
+ /// must wait
/// for an in-flight before disposing
/// its semaphore. Without the fix, the close's _closeLock.Release()
/// would race the dispose and raise .
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task DisposeAsync_WhileCloseInFlight_WaitsForCloseAndDoesNotThrow()
{
@@ -149,6 +152,7 @@ public sealed class GatewaySessionTests
/// from the already-disposed semaphore
/// rather than propagating it.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task DisposeAsync_CalledTwice_DoesNotThrow()
{
@@ -164,7 +168,7 @@ public sealed class GatewaySessionTests
}
///
- /// Issue-1 regression. Concurrent Dispose() calls on the same
+ /// Concurrent Dispose() calls on the same
/// — as can happen when a gRPC stream
/// completion and a client cancellation both fire at the same time — must
/// decrement _activeEventSubscriberCount exactly once, never to −1.
@@ -174,6 +178,7 @@ public sealed class GatewaySessionTests
/// the count must be exactly 0 and a subsequent single-subscriber attach must
/// succeed.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task EventSubscriberLease_ConcurrentDispose_DecrementsCountExactlyOnce()
{
@@ -222,10 +227,11 @@ public sealed class GatewaySessionTests
}
///
- /// Task 8 regression. Single-subscriber mode rejects a SECOND concurrent external
+ /// Single-subscriber mode rejects a SECOND concurrent external
/// attach with —
/// the legacy guard is preserved unchanged when multi-subscriber is disabled.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task AttachEventSubscriber_SingleMode_SecondAttachThrowsAlreadyActive()
{
@@ -244,10 +250,11 @@ public sealed class GatewaySessionTests
}
///
- /// Task 8. Multi-subscriber mode allows exactly cap concurrent external
+ /// Multi-subscriber mode allows exactly cap concurrent external
/// subscribers; the (cap+1)-th attach throws
/// .
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task AttachEventSubscriber_MultiMode_AttachesUpToCapThenThrowsLimitReached()
{
@@ -280,10 +287,11 @@ public sealed class GatewaySessionTests
}
///
- /// Task 8. The gateway-owned INTERNAL dashboard subscriber must NOT consume cap
+ /// The gateway-owned INTERNAL dashboard subscriber must NOT consume cap
/// budget: with the dashboard mirror running, the full cap of external subscribers is
/// still attachable.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task AttachEventSubscriber_MultiMode_DashboardMirrorDoesNotConsumeCap()
{
@@ -322,12 +330,13 @@ public sealed class GatewaySessionTests
}
///
- /// Task 8 concurrency. Many concurrent attaches in multi-subscriber mode must never
+ /// Many concurrent attaches in multi-subscriber mode must never
/// exceed the cap: the count-check-and-increment is atomic under _syncRoot, so
/// exactly cap attaches succeed and the rest throw
/// . The observed
/// count never goes above the cap.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task AttachEventSubscriber_MultiMode_ConcurrentAttaches_NeverExceedCap()
{
@@ -396,9 +405,10 @@ public sealed class GatewaySessionTests
}
///
- /// Task 8. Disposing a subscriber frees a cap slot so a fresh attach succeeds, and a
+ /// Disposing a subscriber frees a cap slot so a fresh attach succeeds, and a
/// double-dispose does not double-free the slot (count integrity preserved).
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task AttachEventSubscriber_MultiMode_DisposeFreesSlotAndDoubleDisposeIsIdempotent()
{
@@ -434,10 +444,11 @@ public sealed class GatewaySessionTests
}
///
- /// Task 11. With a positive detach-grace, dropping the last external subscriber must
+ /// With a positive detach-grace, dropping the last external subscriber must
/// RETAIN the session (it stays , not Closed/Faulted)
/// and record a detached timestamp so the lease monitor can age it out later.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task DetachGrace_LastSubscriberDrops_RetainsSessionAndRecordsDetachedTimestamp()
{
@@ -461,9 +472,10 @@ public sealed class GatewaySessionTests
}
///
- /// Task 11. Advancing the clock past the detach-grace window makes the retained,
+ /// Advancing the clock past the detach-grace window makes the retained,
/// detached session eligible for close ().
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task DetachGrace_ClockAdvancesPastWindow_SessionBecomesEligibleForClose()
{
@@ -487,10 +499,11 @@ public sealed class GatewaySessionTests
}
///
- /// Task 11. Re-attaching a subscriber before the window elapses cancels the grace:
+ /// Re-attaching a subscriber before the window elapses cancels the grace:
/// the detached timestamp clears and a subsequent clock advance does NOT make the
/// session eligible for close.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task DetachGrace_ReattachBeforeExpiry_CancelsGrace()
{
@@ -520,11 +533,12 @@ public sealed class GatewaySessionTests
}
///
- /// Task 11. With detach-grace disabled (0), dropping the last subscriber must
+ /// With detach-grace disabled (0), dropping the last subscriber must
/// match today's behavior: the session stays with no
/// detached timestamp and is never eligible for a detach-grace close — it lingers only
/// until its normal lease expires.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task DetachGrace_Disabled_MatchesTodaysBehavior()
{
@@ -546,13 +560,14 @@ public sealed class GatewaySessionTests
}
///
- /// Server-055 regression. A FAILED first attach (the distributor never registered a
+ /// A FAILED first attach (the distributor never registered a
/// subscriber) must NOT enter the detach-grace window: the catch path's
/// DetachEventSubscriber rolls the reserved slot back to 0 but must not stamp
/// DetachedAtUtc, because the "last subscriber dropped" semantics only apply once
/// a subscriber was successfully registered. A freshly-Ready session whose first attach
/// failed must therefore stay out of grace and never become sweep-eligible on that basis.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task DetachGrace_FailedFirstAttach_DoesNotEnterGrace()
{
@@ -583,11 +598,12 @@ public sealed class GatewaySessionTests
}
///
- /// Task 11. The gateway-owned internal dashboard subscriber must NOT keep a session out
+ /// The gateway-owned internal dashboard subscriber must NOT keep a session out
/// of detach-grace: with only the dashboard mirror attached (and no external gRPC
/// subscriber), dropping the last external subscriber still enters grace and the
/// window still expires.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task DetachGrace_DashboardMirrorAlone_DoesNotPreventGraceEntry()
{
@@ -615,11 +631,12 @@ public sealed class GatewaySessionTests
}
///
- /// Task 11. Validates the TOCTOU fix: TryBeginCloseIfExpired atomically re-checks that no
+ /// Validates the TOCTOU fix: TryBeginCloseIfExpired atomically re-checks that no
/// subscriber has reattached before flipping to Closing. When the grace window has elapsed but
/// a subscriber is attached by the time TryBeginCloseIfExpired runs, it returns false and the
/// session remains Ready.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task TryBeginCloseIfExpired_ReattachedSubscriberWinsRace_DeclinesClose()
{
@@ -749,16 +766,16 @@ public sealed class GatewaySessionTests
private readonly TaskCompletionSource _shutdownStarted = new(TaskCreationOptions.RunContinuationsAsynchronously);
private readonly TaskCompletionSource _shutdownReleased = new(TaskCreationOptions.RunContinuationsAsynchronously);
- /// Gets the session identifier.
+ ///
public string SessionId { get; } = "session-test";
- /// Gets the worker process identifier.
+ ///
public int? ProcessId { get; } = 1234;
- /// Gets or sets the worker client state.
+ ///
public WorkerClientState State { get; private set; } = WorkerClientState.Ready;
- /// Gets the last recorded heartbeat timestamp.
+ ///
public DateTimeOffset LastHeartbeatAt { get; } = DateTimeOffset.UtcNow;
/// Gets the count of shutdown invocations.
@@ -768,6 +785,7 @@ public sealed class GatewaySessionTests
public int DisposeCount { get; private set; }
/// Waits for shutdown to start.
+ /// A task that represents the asynchronous operation.
public Task WaitForShutdownStartAsync()
{
return _shutdownStarted.Task.WaitAsync(TimeSpan.FromSeconds(5));
@@ -811,7 +829,8 @@ public sealed class GatewaySessionTests
State = WorkerClientState.Faulted;
}
- ///
+ /// Releases the fake worker resources and records the invocation.
+ /// A task that represents the asynchronous operation.
public ValueTask DisposeAsync()
{
DisposeCount++;
@@ -821,16 +840,16 @@ public sealed class GatewaySessionTests
private sealed class FakeWorkerClient : IWorkerClient
{
- /// Gets the session identifier.
+ ///
public string SessionId { get; } = "session-test";
- /// Gets the worker process identifier.
+ ///
public int? ProcessId { get; } = 1234;
- /// Gets the worker client state.
+ ///
public WorkerClientState State { get; } = WorkerClientState.Ready;
- /// Gets the last recorded heartbeat timestamp.
+ ///
public DateTimeOffset LastHeartbeatAt { get; } = DateTimeOffset.UtcNow;
/// Gets the count of dispose invocations.
@@ -861,7 +880,8 @@ public sealed class GatewaySessionTests
{
}
- ///
+ /// Releases the fake worker resources and records the invocation.
+ /// A task that represents the asynchronous operation.
public ValueTask DisposeAsync()
{
DisposeCount++;
diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Sessions/SessionEventDistributorTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Sessions/SessionEventDistributorTests.cs
index e3a60a9..1bff529 100644
--- a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Sessions/SessionEventDistributorTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Sessions/SessionEventDistributorTests.cs
@@ -16,6 +16,8 @@ public sealed class SessionEventDistributorTests
{
private static readonly TimeSpan ReadTimeout = TimeSpan.FromSeconds(5);
+ /// Two subscribers registered on the same distributor both receive every fanned event, in order.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task TwoSubscribers_BothReceiveFannedEventsInOrder()
{
@@ -40,6 +42,8 @@ public sealed class SessionEventDistributorTests
Assert.Equal(2ul, b2.WorkerSequence);
}
+ /// Disposing one subscriber's lease stops delivery to it while the other subscriber keeps receiving events.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task DisposingOneLease_StopsItsDelivery_OtherKeepsReceiving()
{
@@ -65,6 +69,8 @@ public sealed class SessionEventDistributorTests
Assert.Equal(2ul, b2.WorkerSequence);
}
+ /// A subscriber registered after the pump has started only receives events emitted after its registration.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task SubscriberRegisteredAfterStart_ReceivesEventsEmittedAfterRegistration()
{
@@ -84,6 +90,8 @@ public sealed class SessionEventDistributorTests
Assert.Equal(2ul, b.WorkerSequence);
}
+ /// Disposing the distributor completes every subscriber channel and stops the pump.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task DisposingDistributor_CompletesAllSubscriberChannels_AndStopsPump()
{
@@ -101,6 +109,8 @@ public sealed class SessionEventDistributorTests
await AssertCompletedAsync(leaseB.Reader);
}
+ /// Calling Register after the distributor has been disposed throws .
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task Register_AfterDispose_ThrowsObjectDisposedException()
{
@@ -113,6 +123,11 @@ public sealed class SessionEventDistributorTests
Assert.Throws(() => distributor.Register());
}
+ ///
+ /// Pins the nested-lock disposal behavior in RegisterWithReplay: calling it after the
+ /// distributor has been disposed throws .
+ ///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task RegisterWithReplay_AfterDispose_ThrowsObjectDisposedException()
{
@@ -134,6 +149,8 @@ public sealed class SessionEventDistributorTests
out _));
}
+ /// When retained events exceed the replay buffer's capacity, the oldest entries are evicted first and the replay reports a gap.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task ReplayBuffer_OverCapacity_EvictsOldestFirst_AndReportsGap()
{
@@ -168,6 +185,8 @@ public sealed class SessionEventDistributorTests
Assert.Equal(new ulong[] { 3, 4, 5 }, replay.Select(e => e.WorkerSequence));
}
+ /// Requesting replay from a sequence still inside the retained window returns only the newer events, with no gap.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task ReplayBuffer_WithinRetainedWindow_ReturnsNewerEvents_NoGap()
{
@@ -194,6 +213,8 @@ public sealed class SessionEventDistributorTests
Assert.Equal(new ulong[] { 3, 4, 5 }, replay.Select(e => e.WorkerSequence));
}
+ /// Retained replay entries older than the retention window are evicted once that window elapses.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task ReplayBuffer_AgedEntries_AreEvictedAfterRetentionElapses()
{
@@ -228,6 +249,8 @@ public sealed class SessionEventDistributorTests
Assert.True(gap);
}
+ /// Requesting replay from a sequence newer than everything retained returns an empty list with no gap.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task ReplayBuffer_AfterSequenceNewerThanAllRetained_ReturnsEmpty_NoGap()
{
@@ -254,6 +277,11 @@ public sealed class SessionEventDistributorTests
Assert.Empty(replay);
}
+ ///
+ /// With replay buffering disabled (capacity 0), a caller behind the highest-seen sequence
+ /// is told there is a gap and gets no replayed events.
+ ///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task ReplayBuffer_Capacity0_AfterSequenceBelowHighestSeen_ReportsGap_NoEvents()
{
@@ -281,6 +309,8 @@ public sealed class SessionEventDistributorTests
Assert.Empty(replay);
}
+ /// With replay buffering disabled (capacity 0), a caller already caught up sees no gap and no events.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task ReplayBuffer_Capacity0_AfterSequenceAtOrAboveHighestSeen_NoGap_NoEvents()
{
@@ -307,6 +337,8 @@ public sealed class SessionEventDistributorTests
Assert.Empty(replay);
}
+ /// When no events have ever been seen, any requested sequence reports no gap and no events.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task ReplayBuffer_NoEventsSeen_AnyAfterSequence_NoGap_NoEvents()
{
@@ -325,6 +357,11 @@ public sealed class SessionEventDistributorTests
Assert.Empty(replay);
}
+ ///
+ /// Requesting replay from with retained events present does not
+ /// falsely report a gap from the wrap-around and yields no new events.
+ ///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task ReplayBuffer_AfterSequenceMaxValue_WithRetainedEvents_NoGap_NoNewEvents()
{
@@ -349,12 +386,15 @@ public sealed class SessionEventDistributorTests
Assert.Empty(replay);
}
+ ///
+ /// Per-subscriber backpressure isolation: one subscriber stops reading and overflows its
+ /// own tiny channel; it is disconnected with an EventQueueOverflow fault while a
+ /// second, healthy subscriber keeps receiving and the pump keeps pumping.
+ ///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task SlowSubscriberOverflow_DisconnectsOnlyThatSubscriber_PumpAndOtherKeepRunning()
{
- // Per-subscriber backpressure isolation (Task 5): one subscriber stops reading and
- // overflows its own tiny channel; it is disconnected with an EventQueueOverflow fault
- // while a second, healthy subscriber keeps receiving and the pump keeps pumping.
Channel source = Channel.CreateUnbounded();
int overflowCalls = 0;
// Separate fields for the bool value and the "set" flag so both can use
@@ -380,7 +420,7 @@ public sealed class SessionEventDistributorTests
singleSubscriberMode: false);
await distributor.StartAsync(CancellationToken.None);
- // Slow subscriber: registered but never read, so its capacity-2 channel fills.
+ // Slow subscriber: registered but never read, so its channel (capacity 2) fills.
using IEventSubscriberLease slow = distributor.Register();
// Healthy subscriber: drains promptly throughout.
using IEventSubscriberLease healthy = distributor.Register();
@@ -399,7 +439,7 @@ public sealed class SessionEventDistributorTests
async () => await DrainUntilFaultAsync(slow.Reader));
Assert.Equal(SessionManagerErrorCode.EventQueueOverflow, fault.ErrorCode);
- // Multi-subscriber mode, so isOnlySubscriber is always false (Task 8 mode-gating).
+ // Multi-subscriber mode, so isOnlySubscriber is always false (mode-gating).
// Use Interlocked.Read / Volatile.Read so the test-thread reads are ordered after the
// pump-thread writes, avoiding a data race by the C# memory model.
Assert.Equal(1, Volatile.Read(ref overflowCalls));
@@ -413,16 +453,19 @@ public sealed class SessionEventDistributorTests
Assert.Equal(11ul, afterOverflow.WorkerSequence);
}
+ ///
+ /// Distributor-level pin for "FailFast with multiple subscribers degrades to
+ /// disconnect-only (no session fault)": in multi-subscriber mode isOnlySubscriber is
+ /// always false (mode-gating), so a FailFast-wired handler must NOT fault the session.
+ /// This test drives the distributor directly (without GatewaySession) in
+ /// multi-subscriber mode with two subscribers and a FailFast-style overflow handler
+ /// seam, overflows the slow one, and asserts (a) isOnlySubscriber==false, (b) the other
+ /// subscriber keeps receiving, and (c) the pump keeps running.
+ ///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task SlowSubscriberOverflow_WithMultipleSubscribers_HandlerSeesIsOnlySubscriberFalse_OtherKeepsReceiving()
{
- // Distributor-level pin for "FailFast with multiple subscribers degrades to
- // disconnect-only (no session fault)": in multi-subscriber mode isOnlySubscriber is
- // always false (Task 8 mode-gating), so a FailFast-wired handler must NOT fault the
- // session. This test drives the distributor directly (without GatewaySession) in
- // multi-subscriber mode with two subscribers and a FailFast-style overflow handler
- // seam, overflows the slow one, and asserts (a) isOnlySubscriber==false, (b) the other
- // subscriber keeps receiving, and (c) the pump keeps running.
Channel source = Channel.CreateUnbounded();
bool handlerFiredWithFalse = false;
bool sessionFaultWouldBeCalled = false; // tracks if a FailFast path would fault
@@ -450,7 +493,7 @@ public sealed class SessionEventDistributorTests
singleSubscriberMode: false);
await distributor.StartAsync(CancellationToken.None);
- // Slow subscriber: never reads, so capacity-2 channel overflows quickly.
+ // Slow subscriber: never reads, so its channel (capacity 2) overflows quickly.
using IEventSubscriberLease slow = distributor.Register();
// Healthy subscriber: drains every event promptly.
using IEventSubscriberLease healthy = distributor.Register();
@@ -478,14 +521,17 @@ public sealed class SessionEventDistributorTests
Assert.Equal(11ul, afterOverflow.WorkerSequence);
}
+ ///
+ /// Verifies that CountExternalSubscribers() excludes the internal dashboard
+ /// subscriber, so a FailFast policy would NOT fault the session even when the internal
+ /// subscriber is the ONLY registered subscriber. The overflow handler receives
+ /// isOnlySubscriber==false (not true) because the overflowing subscriber is internal
+ /// and is therefore excluded from the external-subscriber count.
+ ///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task InternalSubscriberOverflow_HandlerSeesIsOnlySubscriberFalse_ProvingCountExcludesInternal()
{
- // Issue 3: verifies that CountExternalSubscribers() excludes the internal dashboard
- // subscriber, so a FailFast policy would NOT fault the session even when the internal
- // subscriber is the ONLY registered subscriber. The overflow handler receives
- // isOnlySubscriber==false (not true) because the overflowing subscriber is internal
- // and is therefore excluded from the external-subscriber count.
Channel source = Channel.CreateUnbounded();
int observedIsOnlySubscriberSet = 0;
bool observedIsOnlySubscriberValue = false;
@@ -509,7 +555,7 @@ public sealed class SessionEventDistributorTests
// Register ONLY an internal subscriber — no external subscriber is attached.
using IEventSubscriberLease internalLease = distributor.Register(isInternal: true);
- // Push enough events to overflow the capacity-2 internal subscriber channel.
+ // Push enough events to overflow the internal subscriber channel (capacity 2).
for (ulong sequence = 1; sequence <= 10; sequence++)
{
source.Writer.TryWrite(Event(sequence));
@@ -540,13 +586,16 @@ public sealed class SessionEventDistributorTests
"isInternal must be true for a subscriber registered with isInternal: true.");
}
+ ///
+ /// Mode-gating: in single-subscriber mode a lone external subscriber that overflows
+ /// reports isOnlySubscriber==true, so the legacy FailFast session-fault path is
+ /// preserved. The decision is gated on the fixed session mode, NOT a live count, so it
+ /// is race-free.
+ ///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task SingleSubscriberMode_LoneExternalOverflow_HandlerSeesIsOnlySubscriberTrue()
{
- // Task 8 mode-gating: in single-subscriber mode a lone external subscriber that
- // overflows reports isOnlySubscriber==true, so the legacy FailFast session-fault path
- // is preserved. The decision is gated on the fixed session mode, NOT a live count, so
- // it is race-free.
Channel source = Channel.CreateUnbounded();
int observedSet = 0;
bool observedValue = false;
@@ -593,6 +642,11 @@ public sealed class SessionEventDistributorTests
"isOnlySubscriber must be true for a lone external subscriber in single-subscriber mode.");
}
+ ///
+ /// Registering with replay from a sequence still inside the retained window returns the
+ /// newer retained events with no gap, then continues to deliver live events.
+ ///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task RegisterWithReplay_WithinRetainedWindow_ReturnsNewerEvents_NoGap_ThenLive()
{
@@ -631,6 +685,8 @@ public sealed class SessionEventDistributorTests
Assert.Equal(6ul, live.WorkerSequence);
}
+ /// Registering with replay from a sequence below the oldest retained event reports a gap along with the oldest available sequence.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task RegisterWithReplay_BelowOldestRetained_ReportsGap_AndOldestAvailable()
{
@@ -662,6 +718,8 @@ public sealed class SessionEventDistributorTests
Assert.Equal(5ul, liveResume);
}
+ /// When nothing retained is newer than the requested sequence, the live resume watermark equals the requested sequence and no gap is reported.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task RegisterWithReplay_NothingRetainedNewer_LiveResumeEqualsAfterSequence_NoGap()
{
@@ -733,6 +791,7 @@ public sealed class SessionEventDistributorTests
/// GatewaySessionDashboardMirrorTests, where a gRPC subscriber attached after a
/// fast-completing worker stream had already drained.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task Register_AfterSourceCompletes_CompletesLateSubscriberInsteadOfHanging()
{
diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Sessions/SessionManagerBulkTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Sessions/SessionManagerBulkTests.cs
index 7847550..7f7b72d 100644
--- a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Sessions/SessionManagerBulkTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Sessions/SessionManagerBulkTests.cs
@@ -9,7 +9,7 @@ using ZB.MOM.WW.MxGateway.Server.Workers;
namespace ZB.MOM.WW.MxGateway.Tests.Gateway.Sessions;
///
-/// Tests-013: per-method gateway-side coverage for every
+/// Per-method gateway-side coverage for every
/// GatewaySession.*BulkAsync entry point. Each method gets a
/// round-trip test that pins the sent to the
/// worker, the per-entry payload shape, a failure-mode (per-entry failure
@@ -21,6 +21,7 @@ namespace ZB.MOM.WW.MxGateway.Tests.Gateway.Sessions;
public sealed class SessionManagerBulkTests
{
/// Verifies that AddItemBulkAsync forwards the command and returns results.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task AddItemBulkAsync_ForwardsOneAddItemBulkCommandAndReturnsResults()
{
@@ -50,6 +51,7 @@ public sealed class SessionManagerBulkTests
}
/// Verifies that AddItemBulkAsync propagates cancellation.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task AddItemBulkAsync_PropagatesCancellation()
{
@@ -63,6 +65,7 @@ public sealed class SessionManagerBulkTests
}
/// Verifies that AdviseItemBulkAsync forwards the command and returns results.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task AdviseItemBulkAsync_ForwardsOneAdviseItemBulkCommandAndReturnsResults()
{
@@ -90,6 +93,7 @@ public sealed class SessionManagerBulkTests
}
/// Verifies that AdviseItemBulkAsync propagates cancellation.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task AdviseItemBulkAsync_PropagatesCancellation()
{
@@ -103,6 +107,7 @@ public sealed class SessionManagerBulkTests
}
/// Verifies that RemoveItemBulkAsync forwards the command and returns results.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task RemoveItemBulkAsync_ForwardsOneRemoveItemBulkCommandAndReturnsResults()
{
@@ -128,6 +133,7 @@ public sealed class SessionManagerBulkTests
}
/// Verifies that RemoveItemBulkAsync propagates cancellation.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task RemoveItemBulkAsync_PropagatesCancellation()
{
@@ -141,6 +147,7 @@ public sealed class SessionManagerBulkTests
}
/// Verifies that UnAdviseItemBulkAsync forwards the command and returns results.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task UnAdviseItemBulkAsync_ForwardsOneUnAdviseItemBulkCommandAndReturnsResults()
{
@@ -167,6 +174,7 @@ public sealed class SessionManagerBulkTests
}
/// Verifies that UnAdviseItemBulkAsync propagates cancellation.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task UnAdviseItemBulkAsync_PropagatesCancellation()
{
@@ -180,6 +188,7 @@ public sealed class SessionManagerBulkTests
}
/// Verifies that SubscribeBulkAsync surfaces per-entry failures.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task SubscribeBulkAsync_SurfacesPerEntryFailures()
{
@@ -208,6 +217,7 @@ public sealed class SessionManagerBulkTests
}
/// Verifies that SubscribeBulkAsync propagates cancellation.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task SubscribeBulkAsync_PropagatesCancellation()
{
@@ -221,6 +231,7 @@ public sealed class SessionManagerBulkTests
}
/// Verifies that UnsubscribeBulkAsync forwards the command and returns results.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task UnsubscribeBulkAsync_ForwardsOneUnsubscribeBulkCommandAndReturnsResults()
{
@@ -246,6 +257,7 @@ public sealed class SessionManagerBulkTests
}
/// Verifies that UnsubscribeBulkAsync propagates cancellation.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task UnsubscribeBulkAsync_PropagatesCancellation()
{
@@ -259,6 +271,7 @@ public sealed class SessionManagerBulkTests
}
/// Verifies that WriteBulkAsync surfaces per-entry failures.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task WriteBulkAsync_SurfacesPerEntryFailures()
{
@@ -292,6 +305,7 @@ public sealed class SessionManagerBulkTests
}
/// Verifies that WriteBulkAsync propagates cancellation.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task WriteBulkAsync_PropagatesCancellation()
{
@@ -308,6 +322,7 @@ public sealed class SessionManagerBulkTests
}
/// Verifies that Write2BulkAsync forwards the command and preserves timestamp payload.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task Write2BulkAsync_ForwardsOneWrite2BulkCommandAndPreservesTimestampPayload()
{
@@ -351,6 +366,7 @@ public sealed class SessionManagerBulkTests
}
/// Verifies that Write2BulkAsync propagates cancellation.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task Write2BulkAsync_PropagatesCancellation()
{
@@ -376,6 +392,7 @@ public sealed class SessionManagerBulkTests
}
/// Verifies that WriteSecuredBulkAsync forwards the command and preserves credential payload.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task WriteSecuredBulkAsync_ForwardsOneWriteSecuredBulkCommandAndPreservesCredentialPayload()
{
@@ -427,6 +444,7 @@ public sealed class SessionManagerBulkTests
}
/// Verifies that WriteSecuredBulkAsync propagates cancellation.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task WriteSecuredBulkAsync_PropagatesCancellation()
{
@@ -452,7 +470,7 @@ public sealed class SessionManagerBulkTests
}
///
- /// Tests-022: Pin mid-flight cancellation behaviour for at least one bulk
+ /// Pin mid-flight cancellation behaviour for at least one bulk
/// path. Unlike the pre-cancel WriteSecuredBulkAsync_PropagatesCancellation
/// above, this fake's
/// returns a -backed task that does NOT
@@ -463,6 +481,7 @@ public sealed class SessionManagerBulkTests
/// client closing its stream would hit, which the pre-cancel pattern can't
/// exercise.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task WriteSecuredBulkAsync_WhenCancelledMidFlight_ThrowsOperationCanceledForRequestToken()
{
@@ -499,6 +518,7 @@ public sealed class SessionManagerBulkTests
}
/// Verifies that WriteSecured2BulkAsync forwards the command and preserves credential and timestamp payload.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task WriteSecured2BulkAsync_ForwardsOneWriteSecured2BulkCommandAndPreservesCredentialAndTimestampPayload()
{
@@ -547,6 +567,7 @@ public sealed class SessionManagerBulkTests
}
/// Verifies that WriteSecured2BulkAsync propagates cancellation.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task WriteSecured2BulkAsync_PropagatesCancellation()
{
@@ -573,6 +594,7 @@ public sealed class SessionManagerBulkTests
}
/// Verifies that ReadBulkAsync surfaces per-entry failures.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task ReadBulkAsync_SurfacesPerEntryFailures()
{
@@ -619,6 +641,7 @@ public sealed class SessionManagerBulkTests
}
/// Verifies that ReadBulkAsync propagates cancellation.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task ReadBulkAsync_PropagatesCancellation()
{
@@ -779,12 +802,13 @@ public sealed class SessionManagerBulkTests
///
public void Kill(string reason) => State = WorkerClientState.Faulted;
- ///
+ /// Disposes the fake worker client; this fake has no resources to release.
+ /// A task that represents the asynchronous operation.
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
}
///
- /// Mid-flight cancellation fake for Tests-022.
+ /// Mid-flight cancellation fake worker client.
/// signals , registers
/// a cancellation continuation on the caller's ,
/// and parks on a that completes
@@ -858,7 +882,8 @@ public sealed class SessionManagerBulkTests
_invokeCompletion.TrySetCanceled();
}
- ///
+ /// Disposes the fake worker client, cancelling any in-flight invoke.
+ /// A task that represents the asynchronous operation.
public ValueTask DisposeAsync()
{
_invokeCompletion.TrySetCanceled();
diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Sessions/SessionManagerTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Sessions/SessionManagerTests.cs
index 68bbecb..60252fa 100644
--- a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Sessions/SessionManagerTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Sessions/SessionManagerTests.cs
@@ -14,6 +14,7 @@ namespace ZB.MOM.WW.MxGateway.Tests.Gateway.Sessions;
public sealed class SessionManagerTests
{
/// Verifies that opening a session with a ready worker registers the session in ready state.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task OpenSessionAsync_WithWorkerReady_RegistersReadySession()
{
@@ -37,6 +38,7 @@ public sealed class SessionManagerTests
}
/// Verifies that a session opened by an authenticated caller records that caller's API key id in OwnerKeyId.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task OpenSessionAsync_WithOwnerKeyId_RecordsOwnerKeyIdOnSession()
{
@@ -52,6 +54,7 @@ public sealed class SessionManagerTests
}
/// Verifies that a session opened without an owner key id records null in OwnerKeyId.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task OpenSessionAsync_WithNullOwnerKeyId_RecordsNullOwnerKeyIdOnSession()
{
@@ -67,6 +70,7 @@ public sealed class SessionManagerTests
}
/// Verifies that opening a session sets the initial lease expiry from the configured default lease.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task OpenSessionAsync_SetsInitialDefaultLease()
{
@@ -83,6 +87,7 @@ public sealed class SessionManagerTests
}
/// Verifies that session generation creates client correlation ID from client name and session ID.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task OpenSessionAsync_GeneratesClientCorrelationIdFromClientNameAndSessionId()
{
@@ -99,6 +104,7 @@ public sealed class SessionManagerTests
}
/// Verifies that opening a session without a client session name uses the client correlation prefix.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task OpenSessionAsync_WhenClientSessionNameMissing_UsesClientCorrelationPrefix()
{
@@ -114,6 +120,7 @@ public sealed class SessionManagerTests
}
/// Verifies that invoking a command on a ready session forwards the command to the worker.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task InvokeAsync_WhenSessionReady_ForwardsCommandToWorker()
{
@@ -131,6 +138,7 @@ public sealed class SessionManagerTests
}
/// Verifies that invoking a command on a ready session refreshes its lease expiry.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task InvokeAsync_WhenSessionReady_RefreshesLease()
{
@@ -159,6 +167,7 @@ public sealed class SessionManagerTests
}
/// Verifies that gateway session subscribe bulk forwards one bulk command and returns results.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task GatewaySessionSubscribeBulkAsync_ForwardsOneBulkCommandAndReturnsResults()
{
@@ -204,6 +213,7 @@ public sealed class SessionManagerTests
}
/// Verifies that gateway session write bulk forwards one bulk command and returns results.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task GatewaySessionWriteBulkAsync_ForwardsOneBulkCommandAndReturnsResults()
{
@@ -269,6 +279,7 @@ public sealed class SessionManagerTests
}
/// Verifies that gateway session read bulk forwards one bulk command and returns results.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task GatewaySessionReadBulkAsync_ForwardsOneBulkCommandAndReturnsResults()
{
@@ -319,6 +330,7 @@ public sealed class SessionManagerTests
}
/// Verifies that invoking a command on a faulted session rejects the command.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task InvokeAsync_WhenSessionFaulted_RejectsCommand()
{
@@ -338,12 +350,13 @@ public sealed class SessionManagerTests
}
///
- /// Server-030 regression: when the gateway-side SessionState is
+ /// When the gateway-side SessionState is
/// Ready but the worker client's own state is not, the diagnostic
/// must surface both states so the mismatch is actionable instead of
/// producing a self-contradictory "Session ... is not ready. Current
/// state is Ready." message.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task InvokeAsync_WhenWorkerNotReadyButSessionReady_DiagnosticIncludesBothStates()
{
@@ -372,6 +385,7 @@ public sealed class SessionManagerTests
/// Handshaking but flips to Ready within the timeout window must let the
/// command through rather than fail fast.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task InvokeAsync_WhenWorkerHandshakingThenReadyWithinTimeout_Succeeds()
{
@@ -402,6 +416,7 @@ public sealed class SessionManagerTests
/// A terminal worker state (Faulted) must fail fast even with a positive
/// worker-ready wait timeout, surfacing both states without burning the timeout.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task InvokeAsync_WhenWorkerFaulted_FailsFastWithBothStates()
{
@@ -431,6 +446,7 @@ public sealed class SessionManagerTests
/// When the worker stays transiently not-ready for the whole (small) timeout window,
/// the invoke fails after roughly the timeout with both states surfaced.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task InvokeAsync_WhenTimeoutElapsesStillNotReady_FailsWithBothStates()
{
@@ -460,6 +476,7 @@ public sealed class SessionManagerTests
/// Pins the default (timeout == 0) behavior: a transiently Handshaking worker
/// fails fast immediately, byte-for-byte like the original fail-fast path.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task InvokeAsync_WhenTimeoutZero_FailsFastUnchanged()
{
@@ -486,6 +503,7 @@ public sealed class SessionManagerTests
}
/// Verifies that closing a session removes it from the registry.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task CloseSessionAsync_RemovesClosedSession()
{
@@ -507,6 +525,7 @@ public sealed class SessionManagerTests
}
/// Verifies that closing a session kills the worker when shutdown fails.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task CloseSessionAsync_WhenWorkerShutdownFails_KillsWorker()
{
@@ -528,6 +547,7 @@ public sealed class SessionManagerTests
}
/// Verifies that when worker shutdown fails, the session is removed and the slot is released.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task CloseSessionAsync_WhenWorkerShutdownFails_RemovesSessionAndReleasesSlot()
{
@@ -567,15 +587,13 @@ public sealed class SessionManagerTests
Assert.Equal(1, failingWorkerClient.KillCount);
Assert.Equal(1, failingWorkerClient.DisposeCount);
GatewayMetricsSnapshot snapshot = metrics.GetSnapshot();
- // Server-046: a close-that-failed now accounts as SessionClosed (counter += 1) rather
- // than SessionRemoved (gauge -= 1, counter unchanged). The session is being removed
- // from the registry on this path, so it must show up in the closed count.
Assert.Equal(1, snapshot.SessionsClosed);
Assert.False(snapshot.EventsBySession.ContainsKey(firstSession.SessionId));
Assert.Equal(1, snapshot.OpenSessions);
}
/// Verifies that when the second close is canceled, the session is not removed if owned by the first close.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task CloseSessionAsync_WhenSecondCloseIsCanceled_DoesNotRemoveSessionOwnedByFirstClose()
{
@@ -626,10 +644,8 @@ public sealed class SessionManagerTests
///
/// Verifies that killing a worker removes the session from the registry without calling shutdown.
- /// Tests-028: also pins the reason argument propagating through
- /// SessionManager.KillWorkerAsync → session.KillWorker(reason) →
- /// IWorkerClient.Kill(reason).
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task KillWorkerAsync_KillsWorkerAndRemovesSession()
{
@@ -651,11 +667,12 @@ public sealed class SessionManagerTests
}
///
- /// Tests-028: guards its reason argument with
+ /// guards its reason argument with
/// . A blank or whitespace reason must throw
/// before any session lookup or worker call runs.
///
/// A blank or whitespace reason string.
+ /// A task that represents the asynchronous operation.
[Theory]
[InlineData("")]
[InlineData(" ")]
@@ -674,10 +691,11 @@ public sealed class SessionManagerTests
}
///
- /// Tests-028: also rejects null.
+ /// also rejects null.
/// with cannot carry null for a
/// non-nullable string parameter on .NET 10, so the null case is its own fact.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task KillWorkerAsync_WithNullReason_ThrowsArgumentNullException()
{
@@ -693,6 +711,7 @@ public sealed class SessionManagerTests
}
/// Verifies that killing the worker for an unknown session raises SessionNotFound.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task KillWorkerAsync_WhenSessionMissing_ThrowsSessionNotFound()
{
@@ -705,10 +724,10 @@ public sealed class SessionManagerTests
}
///
- /// Regression for Server-044: when session.KillWorker throws, the catch path must still
- /// decrement mxgateway.sessions.open (parity with the Server-006 fix in
- /// OpenSessionAsync). Without the fix the gauge leaks one open session per failed kill.
+ /// When session.KillWorker throws, the catch path must still
+ /// decrement mxgateway.sessions.open. Without the fix the gauge leaks one open session per failed kill.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task KillWorkerAsync_WhenSessionKillThrows_DecrementsOpenSessionGauge()
{
@@ -738,10 +757,11 @@ public sealed class SessionManagerTests
}
///
- /// Regression for Server-045 / Server-048: concurrent kills on the same session must not
+ /// Concurrent kills on the same session must not
/// double-increment mxgateway.sessions.closed. The first kill wins, the second
/// observes wasClosed == true (or a missing session after removal) and short-circuits.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task KillWorkerAsync_ConcurrentCallsOnSameSession_CountClosedExactlyOnce()
{
@@ -777,12 +797,13 @@ public sealed class SessionManagerTests
}
///
- /// Regression for Server-046: ShutdownAsync's graceful-close fallback (which calls
+ /// ShutdownAsync's graceful-close fallback (which calls
/// KillWorker + RemoveSessionAsync when CloseSessionCoreAsync throws)
/// must still account a successful close: both the open-session gauge must drop to zero AND
/// the mxgateway.sessions.closed counter must increment. Without the fix, the
/// graceful-close failure path under-counts the closed counter.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task ShutdownAsync_WhenSessionCloseThrows_StillDecrementsOpenSessionGaugeAndIncrementsClosedCounter()
{
@@ -812,6 +833,7 @@ public sealed class SessionManagerTests
}
/// Verifies that when worker creation fails, the session is removed from the registry.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task OpenSessionAsync_WhenWorkerCreationFails_RemovesSessionFromRegistry()
{
@@ -832,6 +854,7 @@ public sealed class SessionManagerTests
}
/// Verifies that closing expired leases only closes expired sessions.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task CloseExpiredLeasesAsync_ClosesExpiredSessionsOnly()
{
@@ -855,6 +878,7 @@ public sealed class SessionManagerTests
}
/// Verifies that an expired-lease sweep leaves a session with an active event subscriber open.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task CloseExpiredLeasesAsync_DoesNotCloseActiveEventSubscriber()
{
@@ -873,10 +897,11 @@ public sealed class SessionManagerTests
}
///
- /// Task 11. With detach-grace enabled, a session whose last external subscriber dropped
+ /// With detach-grace enabled, a session whose last external subscriber dropped
/// and whose detach-grace window has elapsed is closed by the lease sweep exactly like an
/// expired-lease session — even though its normal lease is still far in the future.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task CloseExpiredLeasesAsync_ClosesSessionWhoseDetachGraceWindowExpired()
{
@@ -909,11 +934,12 @@ public sealed class SessionManagerTests
}
///
- /// Task 11. TOCTOU race: a session whose detach-grace window has expired but that
+ /// TOCTOU race: a session whose detach-grace window has expired but that
/// reattaches an external subscriber before the sweeper calls CloseSessionCoreAsync is
/// NOT closed — it remains Ready and usable. This validates that TryBeginCloseIfExpired
/// re-checks eligibility atomically so a reconnect that wins the race cancels the close.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task CloseExpiredLeasesAsync_DoesNotCloseSessionThatReattachedBeforeSweepCloses()
{
@@ -950,6 +976,7 @@ public sealed class SessionManagerTests
}
/// Verifies that shutdown closes all registered sessions.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task ShutdownAsync_ClosesAllRegisteredSessions()
{
@@ -1097,16 +1124,16 @@ public sealed class SessionManagerTests
private sealed class FakeWorkerClient : IWorkerClient
{
- /// Gets the session ID for the fake worker client.
+ ///
public string SessionId { get; init; } = "session-1";
- /// Gets the process ID for the fake worker client.
+ ///
public int? ProcessId { get; init; } = 1234;
- /// Gets or sets the state of the fake worker client.
+ ///
public WorkerClientState State { get; set; } = WorkerClientState.Ready;
- /// Gets the last heartbeat timestamp for the fake worker client.
+ ///
public DateTimeOffset LastHeartbeatAt { get; init; } = DateTimeOffset.UtcNow;
/// Gets the number of times invoke was called on the fake worker client.
@@ -1118,13 +1145,7 @@ public sealed class SessionManagerTests
/// Gets the number of times kill was called on the fake worker client.
public int KillCount { get; private set; }
- ///
- /// Gets the last reason argument observed by . Tests-028:
- /// pins the reason-string propagation through
- /// SessionManager.KillWorkerAsync → session.KillWorker(reason) →
- /// IWorkerClient.Kill(reason). Without this, the chain could silently
- /// drop or substitute the reason argument and existing tests would still pass.
- ///
+ /// Gets the last reason argument observed by .
public string? LastKillReason { get; private set; }
/// Gets the number of times dispose was called on the fake worker client.
@@ -1222,7 +1243,8 @@ public sealed class SessionManagerTests
State = WorkerClientState.Faulted;
}
- ///
+ /// Records that the fake worker client was disposed.
+ /// A task that represents the asynchronous operation.
public ValueTask DisposeAsync()
{
DisposeCount++;
@@ -1230,6 +1252,7 @@ public sealed class SessionManagerTests
}
/// Waits for shutdown to start on the fake worker client.
+ /// A task that represents the asynchronous operation.
public Task WaitForShutdownStartAsync()
{
return ShutdownStarted.Task.WaitAsync(TimeSpan.FromSeconds(5));
diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Sessions/SessionWorkerClientFactoryFakeWorkerTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Sessions/SessionWorkerClientFactoryFakeWorkerTests.cs
index 11b09d5..13534a0 100644
--- a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Sessions/SessionWorkerClientFactoryFakeWorkerTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Sessions/SessionWorkerClientFactoryFakeWorkerTests.cs
@@ -21,6 +21,7 @@ public sealed class SessionWorkerClientFactoryFakeWorkerTests : IAsyncDisposable
/// Awaits every scripted worker task so an unhandled exception fails the owning test
/// instead of surfacing later as an unobserved .
///
+ /// A task that represents the asynchronous operation.
public async ValueTask DisposeAsync()
{
foreach (IWorkerTaskLauncher launcher in _launchers)
@@ -30,6 +31,7 @@ public sealed class SessionWorkerClientFactoryFakeWorkerTests : IAsyncDisposable
}
/// Verifies that the factory creates a ready worker client with a scripted fake worker process.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task CreateAsync_WithScriptedFakeWorker_ReturnsReadyClient()
{
@@ -63,6 +65,7 @@ public sealed class SessionWorkerClientFactoryFakeWorkerTests : IAsyncDisposable
}
/// Verifies that a failed fake worker startup throws a worker client exception.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task CreateAsync_WhenFakeWorkerStartupFails_ThrowsWorkerClientException()
{
@@ -83,6 +86,7 @@ public sealed class SessionWorkerClientFactoryFakeWorkerTests : IAsyncDisposable
}
/// Verifies that a worker that never sends ready times out and is killed.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task CreateAsync_WhenFakeWorkerNeverSendsReady_TimesOutAndKillsWorker()
{
@@ -168,6 +172,7 @@ public sealed class SessionWorkerClientFactoryFakeWorkerTests : IAsyncDisposable
/// teardown faults expected when the worker client kills or disposes the worker.
///
/// Maximum time to wait for the worker task.
+ /// A task that represents the asynchronous operation.
Task ObserveWorkerTaskAsync(TimeSpan timeout);
}
diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Sessions/SparseArrayExpanderTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Sessions/SparseArrayExpanderTests.cs
index 668230d..dd5449e 100644
--- a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Sessions/SparseArrayExpanderTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Sessions/SparseArrayExpanderTests.cs
@@ -26,6 +26,7 @@ public sealed class SparseArrayExpanderTests
return new MxValue { SparseArrayValue = sparse };
}
+ /// Verifies that expanding an Int32 sparse array fills unset indices with the default and applies the given element.
[Fact]
public void Expand_Int32_FillsDefaultsAndSetsElement()
{
@@ -43,6 +44,7 @@ public sealed class SparseArrayExpanderTests
Assert.Equal(new[] { 0, 7, 0, 0 }, value.ArrayValue.Int32Values.Values);
}
+ /// Verifies that expanding a Boolean sparse array with no elements fills every index with the default value false.
[Fact]
public void Expand_Boolean_EmptyElements_AllDefaultFalse()
{
@@ -54,6 +56,7 @@ public sealed class SparseArrayExpanderTests
Assert.Equal(new[] { false, false, false }, value.ArrayValue.BoolValues.Values);
}
+ /// Verifies that expanding a sparse array with a zero total length throws an invalid-argument .
[Fact]
public void Expand_ZeroTotalLength_Throws()
{
@@ -63,6 +66,7 @@ public sealed class SparseArrayExpanderTests
Assert.Equal(StatusCode.InvalidArgument, ex.StatusCode);
}
+ /// Verifies that an element index at or beyond the total length throws an invalid-argument .
[Fact]
public void Expand_IndexOutOfRange_Throws()
{
@@ -75,6 +79,7 @@ public sealed class SparseArrayExpanderTests
Assert.Equal(StatusCode.InvalidArgument, ex.StatusCode);
}
+ /// Verifies that two sparse elements sharing the same index throw an invalid-argument .
[Fact]
public void Expand_DuplicateIndex_Throws()
{
@@ -88,6 +93,7 @@ public sealed class SparseArrayExpanderTests
Assert.Equal(StatusCode.InvalidArgument, ex.StatusCode);
}
+ /// Verifies that an unspecified element data type throws an invalid-argument .
[Fact]
public void Expand_UnsupportedElementType_Throws()
{
@@ -97,6 +103,7 @@ public sealed class SparseArrayExpanderTests
Assert.Equal(StatusCode.InvalidArgument, ex.StatusCode);
}
+ /// Verifies that an element value whose kind does not match the declared element data type throws an invalid-argument .
[Fact]
public void Expand_ElementValueKindMismatch_Throws()
{
@@ -109,6 +116,7 @@ public sealed class SparseArrayExpanderTests
Assert.Equal(StatusCode.InvalidArgument, ex.StatusCode);
}
+ /// Verifies that expanding a String sparse array fills unset indices with an empty string default.
[Fact]
public void Expand_String_FillsEmptyStringDefault()
{
@@ -123,6 +131,7 @@ public sealed class SparseArrayExpanderTests
Assert.Equal(new[] { "a", string.Empty }, value.ArrayValue.StringValues.Values);
}
+ /// Verifies that expanding a Time sparse array fills unset indices with the epoch timestamp default.
[Fact]
public void Expand_Time_FillsEpochDefault()
{
@@ -140,6 +149,7 @@ public sealed class SparseArrayExpanderTests
Assert.Equal(5, value.ArrayValue.TimestampValues.Values[1].Seconds);
}
+ /// Verifies that expanding a Double sparse array fills defaults and applies the given element.
[Fact]
public void Expand_Double_HappyPath()
{
@@ -154,6 +164,7 @@ public sealed class SparseArrayExpanderTests
Assert.Equal(new[] { 0d, 0d, 1.5 }, value.ArrayValue.DoubleValues.Values);
}
+ /// Verifies that expanding a Float sparse array fills defaults and applies the given element.
[Fact]
public void Expand_Float_HappyPath()
{
@@ -168,6 +179,7 @@ public sealed class SparseArrayExpanderTests
Assert.Equal(new[] { 2.5f, 0f }, value.ArrayValue.FloatValues.Values);
}
+ /// Verifies that an Integer sparse array whose element is a 64-bit value expands to the Int64 array representation.
[Fact]
public void Expand_Int64_WhenElementIsInt64()
{
@@ -182,6 +194,7 @@ public sealed class SparseArrayExpanderTests
Assert.Equal(new[] { 0L, 0L, 9_000_000_000L }, value.ArrayValue.Int64Values.Values);
}
+ /// Verifies that expanding a non-sparse-array value is a no-op, leaving the original value unchanged.
[Fact]
public void Expand_NonSparseValue_NoOps()
{
@@ -193,12 +206,14 @@ public sealed class SparseArrayExpanderTests
Assert.Equal(42, value.Int32Value);
}
+ /// Verifies that expanding a null value throws .
[Fact]
public void Expand_NullValue_ThrowsArgumentNull()
{
Assert.Throws(() => SparseArrayExpander.Expand(null!));
}
+ /// Verifies that a total length exceeding the maximum supported array length throws an invalid-argument .
[Fact]
public void Expand_TotalLengthExceedsMaxArrayLength_Throws()
{
diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Workers/FakeWorkerHarnessTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Workers/FakeWorkerHarnessTests.cs
index cb6c6d7..44f3644 100644
--- a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Workers/FakeWorkerHarnessTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Workers/FakeWorkerHarnessTests.cs
@@ -11,6 +11,7 @@ public sealed class FakeWorkerHarnessTests
private static readonly TimeSpan TestTimeout = TimeSpan.FromSeconds(5);
/// Verifies that completing startup with hello and ready transitions the client to ready state.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task CompleteStartupAsync_WithHelloAndReady_TransitionsClientToReady()
{
@@ -28,6 +29,7 @@ public sealed class FakeWorkerHarnessTests
}
/// Verifies that a protocol version mismatch during startup fails the client.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task StartAsync_WithProtocolMismatch_FailsStartup()
{
@@ -47,6 +49,7 @@ public sealed class FakeWorkerHarnessTests
}
/// Verifies that a scripted reply completes a pending command invocation.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task InvokeAsync_WithScriptedReply_CompletesCommand()
{
@@ -69,6 +72,7 @@ public sealed class FakeWorkerHarnessTests
}
/// Verifies that scripted events are yielded in order through the event stream.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task ReadEventsAsync_WithScriptedEvents_YieldsOrderedEvents()
{
@@ -93,6 +97,7 @@ public sealed class FakeWorkerHarnessTests
}
/// Verifies that a scripted fault from the worker faults the client.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task ReadLoop_WithScriptedFault_FaultsClient()
{
@@ -116,6 +121,7 @@ public sealed class FakeWorkerHarnessTests
/// so the timestamp advance is deterministic rather
/// than relying on a wall-clock Task.Delay exceeding clock resolution.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task SendHeartbeatAsync_UpdatesClientHeartbeatState()
{
@@ -138,6 +144,7 @@ public sealed class FakeWorkerHarnessTests
}
/// Verifies that a hung worker times out pending command invocations.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task InvokeAsync_WithHungWorker_TimesOutPendingCommand()
{
@@ -159,6 +166,7 @@ public sealed class FakeWorkerHarnessTests
}
/// Verifies that a malformed frame in the read loop faults the client.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task ReadLoop_WithMalformedFrame_FaultsClient()
{
@@ -176,6 +184,7 @@ public sealed class FakeWorkerHarnessTests
}
/// Verifies that a shutdown acknowledgment from the worker closes the client.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task ShutdownAsync_WithShutdownAck_ClosesClient()
{
@@ -196,6 +205,7 @@ public sealed class FakeWorkerHarnessTests
/// Verifies that RespondToControlCommandAsync echoes the Ping message back
/// in the DiagnosticMessage field, matching the real worker's ping reply shape.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task RespondToControlCommandAsync_Ping_EchoesMessageInDiagnostic()
{
@@ -220,6 +230,7 @@ public sealed class FakeWorkerHarnessTests
/// Verifies that RespondToControlCommandAsync returns a SessionStateReply
/// with state Ready for a GetSessionState command.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task RespondToControlCommandAsync_GetSessionState_ReturnsReadyState()
{
@@ -245,6 +256,7 @@ public sealed class FakeWorkerHarnessTests
/// Verifies that RespondToControlCommandAsync returns a WorkerInfoReply
/// with the fake worker's process ID, version, and MXAccess identifiers.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task RespondToControlCommandAsync_GetWorkerInfo_ReturnsFakeWorkerInfo()
{
@@ -273,6 +285,7 @@ public sealed class FakeWorkerHarnessTests
/// Verifies that RespondToControlCommandAsync returns an empty DrainEventsReply
/// for a DrainEvents command (the fake harness has no queued events).
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task RespondToControlCommandAsync_DrainEvents_ReturnsEmptyReply()
{
@@ -298,6 +311,7 @@ public sealed class FakeWorkerHarnessTests
/// Verifies that RespondToControlCommandAsync for ShutdownWorker sends an OK
/// reply followed by a WorkerShutdownAck, which closes the client.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task RespondToControlCommandAsync_ShutdownWorker_SendsReplyThenAck()
{
diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Workers/Fakes/FakeWorkerHarness.cs b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Workers/Fakes/FakeWorkerHarness.cs
index 6aee6a2..10c5d05 100644
--- a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Workers/Fakes/FakeWorkerHarness.cs
+++ b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Workers/Fakes/FakeWorkerHarness.cs
@@ -52,6 +52,7 @@ public sealed class FakeWorkerHarness : IAsyncDisposable
/// Protocol version for frame communication.
/// Maximum message size in bytes.
/// Token to cancel the asynchronous operation.
+ /// The connected fake worker harness.
public static async Task CreateConnectedPairAsync(
string sessionId = DefaultSessionId,
string nonce = DefaultNonce,
@@ -87,6 +88,7 @@ public sealed class FakeWorkerHarness : IAsyncDisposable
/// Protocol version for frame communication.
/// Maximum message size in bytes.
/// Token to cancel the asynchronous operation.
+ /// The connected fake worker harness.
public static async Task ConnectToGatewayPipeAsync(
string sessionId,
string nonce,
@@ -205,6 +207,7 @@ public sealed class FakeWorkerHarness : IAsyncDisposable
/// Protocol version override.
/// Nonce override.
/// Token to cancel the asynchronous operation.
+ /// A task that represents the asynchronous operation.
public async Task SendWorkerHelloAsync(
int workerProcessId = DefaultWorkerProcessId,
string workerVersion = "fake-worker",
@@ -230,6 +233,7 @@ public sealed class FakeWorkerHarness : IAsyncDisposable
/// MXAccess COM ProgID.
/// MXAccess COM CLSID.
/// Token to cancel the asynchronous operation.
+ /// A task that represents the asynchronous operation.
public async Task SendWorkerReadyAsync(
int workerProcessId = DefaultWorkerProcessId,
string mxaccessProgid = "LMXProxy.LMXProxyServer.1",
@@ -255,6 +259,7 @@ public sealed class FakeWorkerHarness : IAsyncDisposable
/// Human-readable status message.
/// Optional callback to customize the reply.
/// Token to cancel the asynchronous operation.
+ /// A task that represents the asynchronous operation.
public async Task ReplyToCommandAsync(
WorkerEnvelope commandEnvelope,
ProtocolStatusCode statusCode = ProtocolStatusCode.Ok,
@@ -296,6 +301,7 @@ public sealed class FakeWorkerHarness : IAsyncDisposable
/// Family of the event to emit.
/// Token to cancel the asynchronous operation.
/// Optional callback to customize the event.
+ /// A task that represents the asynchronous operation.
public async Task EmitEventAsync(
MxEventFamily family,
CancellationToken cancellationToken = default,
@@ -325,6 +331,7 @@ public sealed class FakeWorkerHarness : IAsyncDisposable
/// Category of the fault.
/// Diagnostic message describing the fault.
/// Token to cancel the asynchronous operation.
+ /// A task that represents the asynchronous operation.
public async Task EmitFaultAsync(
WorkerFaultCategory category,
string diagnosticMessage,
@@ -350,6 +357,7 @@ public sealed class FakeWorkerHarness : IAsyncDisposable
/// Current worker state.
/// Token to cancel the asynchronous operation.
/// Optional callback to customize the heartbeat.
+ /// A task that represents the asynchronous operation.
public async Task SendHeartbeatAsync(
WorkerState state = WorkerState.Ready,
CancellationToken cancellationToken = default,
@@ -373,6 +381,7 @@ public sealed class FakeWorkerHarness : IAsyncDisposable
/// Sends a shutdown acknowledgment message to the gateway.
/// Protocol status code for the acknowledgment.
/// Token to cancel the asynchronous operation.
+ /// A task that represents the asynchronous operation.
public async Task SendShutdownAckAsync(
ProtocolStatusCode statusCode = ProtocolStatusCode.Ok,
CancellationToken cancellationToken = default)
@@ -506,6 +515,7 @@ public sealed class FakeWorkerHarness : IAsyncDisposable
/// Writes a malformed payload directly to the worker stream.
/// Malformed payload bytes to write.
/// Token to cancel the asynchronous operation.
+ /// A task that represents the asynchronous operation.
public async Task WriteMalformedPayloadAsync(
ReadOnlyMemory payload,
CancellationToken cancellationToken = default)
@@ -524,6 +534,7 @@ public sealed class FakeWorkerHarness : IAsyncDisposable
/// Writes an oversized frame header to the worker stream for testing frame size limits.
/// Length of the oversized payload in bytes.
/// Token to cancel the asynchronous operation.
+ /// A task that represents the asynchronous operation.
public async Task WriteOversizedFrameHeaderAsync(
uint payloadLength,
CancellationToken cancellationToken = default)
@@ -542,6 +553,7 @@ public sealed class FakeWorkerHarness : IAsyncDisposable
}
/// Disposes the worker-side stream.
+ /// A task that represents the asynchronous operation.
public async ValueTask DisposeWorkerSideAsync()
{
if (_workerSideDisposed)
@@ -553,7 +565,8 @@ public sealed class FakeWorkerHarness : IAsyncDisposable
_workerSideDisposed = true;
}
- ///
+ /// Disposes the worker-side and (if owned) gateway-side pipe streams.
+ /// A task that represents the asynchronous operation.
public async ValueTask DisposeAsync()
{
await DisposeWorkerSideAsync().ConfigureAwait(false);
diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Workers/OrphanWorkerTerminatorTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Workers/OrphanWorkerTerminatorTests.cs
index 3f407a7..709f16c 100644
--- a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Workers/OrphanWorkerTerminatorTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Workers/OrphanWorkerTerminatorTests.cs
@@ -6,7 +6,7 @@ using ZB.MOM.WW.MxGateway.Server.Workers;
namespace ZB.MOM.WW.MxGateway.Tests.Gateway.Workers;
///
-/// Server-002 regression: per gateway.md the gateway must terminate
+/// Per gateway.md the gateway must terminate
/// orphaned worker processes on startup. These tests pin that the terminator
/// kills leftover workers (matched by executable path, or by image name when
/// the path is unreadable) without touching unrelated processes or itself.
@@ -129,12 +129,10 @@ public sealed class OrphanWorkerTerminatorTests
/// Gets or sets the process ID that should throw when killed.
public int? ThrowOnKillProcessId { get; init; }
- /// Gets the list of running processes by name.
- /// The process name to search for.
+ ///
public IReadOnlyList GetProcessesByName(string processName) => processes;
- /// Kills the specified process or records the kill attempt.
- /// The process identifier to kill.
+ ///
public void Kill(int processId)
{
if (ThrowOnKillProcessId == processId)
diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Workers/WorkerClientTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Workers/WorkerClientTests.cs
index 9750c14..cbf1ef3 100644
--- a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Workers/WorkerClientTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Workers/WorkerClientTests.cs
@@ -16,6 +16,7 @@ public sealed class WorkerClientTests
private static readonly TimeSpan TestTimeout = TimeSpan.FromSeconds(5);
/// Verifies that StartAsync enters ready state after receiving worker hello and ready messages.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task StartAsync_WithWorkerHelloAndReady_EntersReadyState()
{
@@ -29,6 +30,7 @@ public sealed class WorkerClientTests
}
/// Verifies that InvokeAsync completes a pending command when a matching reply arrives.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task InvokeAsync_WithMatchingReply_CompletesPendingCommand()
{
@@ -55,6 +57,7 @@ public sealed class WorkerClientTests
}
/// Verifies that InvokeAsync ignores late replies and keeps the client ready.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task InvokeAsync_WithLateReply_IgnoresLateReplyAndKeepsClientReady()
{
@@ -93,6 +96,7 @@ public sealed class WorkerClientTests
}
/// Verifies that ReadEventsAsync yields events in pipe order from the worker.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task ReadEventsAsync_WithWorkerEvents_YieldsEventsInPipeOrder()
{
@@ -119,6 +123,7 @@ public sealed class WorkerClientTests
}
/// Verifies that the read loop faults the client when the event queue overflows.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task ReadLoop_WhenEventQueueOverflows_FaultsClient()
{
@@ -154,6 +159,7 @@ public sealed class WorkerClientTests
/// Faulted state before it calls KillOwnedProcess, so a state-based
/// wait can observe Faulted while KillCount is still 0.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task ReadLoop_WhenClientFaults_KillsOwnedWorkerProcess()
{
@@ -192,6 +198,7 @@ public sealed class WorkerClientTests
/// invoke task with a carrying the
/// pipe-disconnected error code rather than hanging until the command timeout.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task InvokeAsync_WhenPipeDisconnectsMidCommand_FailsPendingInvokeWithPipeDisconnected()
{
@@ -223,6 +230,7 @@ public sealed class WorkerClientTests
/// task with a carrying the worker-faulted
/// error code.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task InvokeAsync_WhenWorkerFaultsMidCommand_FailsPendingInvokeWithWorkerFaulted()
{
@@ -248,6 +256,7 @@ public sealed class WorkerClientTests
}
/// Verifies that pipe disconnect faults the client.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task ReadLoop_WhenPipeDisconnects_FaultsClient()
{
@@ -265,6 +274,7 @@ public sealed class WorkerClientTests
}
/// Verifies that the read loop stops the running worker metric when the pipe disconnects.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task ReadLoop_WhenPipeDisconnects_StopsRunningWorkerMetric()
{
@@ -288,6 +298,7 @@ public sealed class WorkerClientTests
}
/// Verifies that DisposeAsync returns within a bounded timeout when the pipe read is blocked.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task DisposeAsync_WhenPipeReadIsBlocked_ReturnsWithinBoundedTimeout()
{
@@ -304,7 +315,8 @@ public sealed class WorkerClientTests
$"DisposeAsync took {elapsed.TotalMilliseconds:N0}ms.");
}
- /// Verifies that the read loop updates the last heartbeat and worker process when a heartbeat arrives.
+ /// Verifies that DisposeAsync kills the still-running owned worker process before disposing.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task DisposeAsync_WhenOwnedWorkerStillRuns_KillsProcessBeforeDisposing()
{
@@ -325,6 +337,7 @@ public sealed class WorkerClientTests
/// deterministic instead of relying on a wall-clock Task.Delay exceeding
/// resolution.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task ReadLoop_WhenHeartbeatArrives_UpdatesLastHeartbeatAndWorkerProcess()
{
@@ -352,6 +365,7 @@ public sealed class WorkerClientTests
/// timer stays on the real clock and
/// observes the manually-advanced grace on its next tick.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task HeartbeatMonitor_WhenHeartbeatExpires_FaultsClient()
{
@@ -378,13 +392,14 @@ public sealed class WorkerClientTests
}
///
- /// Server-031 regression: while a command is in flight on the
+ /// While a command is in flight on the
/// gateway↔worker pipe and the oldest pending command is younger
/// than , the
/// heartbeat watchdog must NOT fault on heartbeat-expired alone — the
/// gap is more likely caused by pipe-write contention than by a hung
- /// worker. Mirrors Worker-023 on the worker side.
+ /// worker.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task HeartbeatMonitor_WhenCommandInFlightWithinCeiling_DoesNotFaultOnExpiredHeartbeat()
{
@@ -422,11 +437,12 @@ public sealed class WorkerClientTests
}
///
- /// Server-031 regression: once the oldest pending command exceeds
+ /// Once the oldest pending command exceeds
/// , the
/// heartbeat watchdog fires anyway — a truly stuck COM call shouldn't
/// keep the watchdog suppressed indefinitely.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task HeartbeatMonitor_WhenPendingCommandExceedsStuckCeiling_FaultsClient()
{
@@ -463,7 +479,7 @@ public sealed class WorkerClientTests
}
///
- /// Server-032 regression: a transient burst that exceeds
+ /// A transient burst that exceeds
/// must be
/// absorbed for up to
/// (the channel is configured for BoundedChannelFullMode.Wait);
@@ -471,6 +487,7 @@ public sealed class WorkerClientTests
/// and the diagnostic must name the channel capacity, depth, and
/// actionable remediation.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task EnqueueWorkerEvent_WhenChannelFullPastTimeout_FaultsWithRichDiagnostic()
{
@@ -722,6 +739,7 @@ public sealed class WorkerClientTests
public WorkerFrameWriter WorkerWriter { get; }
/// Creates a connected pipe pair for testing.
+ /// The connected .
public static async Task CreateAsync()
{
string pipeName = $"mxaccessgw-workerclient-tests-{Guid.NewGuid():N}";
@@ -745,6 +763,7 @@ public sealed class WorkerClientTests
}
/// Disposes the worker side of the pipe.
+ /// A task that represents the asynchronous operation.
public async ValueTask DisposeWorkerSideAsync()
{
if (_workerSideDisposed)
@@ -757,6 +776,7 @@ public sealed class WorkerClientTests
}
/// Disposes the duplex stream.
+ /// A task that represents the asynchronous operation.
public async ValueTask DisposeAsync()
{
await DisposeWorkerSideAsync();
diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Workers/WorkerExecutableValidatorTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Workers/WorkerExecutableValidatorTests.cs
index 6f94e73..70dd8ea 100644
--- a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Workers/WorkerExecutableValidatorTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Workers/WorkerExecutableValidatorTests.cs
@@ -5,8 +5,8 @@ using ZB.MOM.WW.MxGateway.Server.Workers;
namespace ZB.MOM.WW.MxGateway.Tests.Gateway.Workers;
///
-/// Coverage for PE-header architecture parsing
-/// (finding Server-013). The validator reads the DOS MZ stub, follows the PE
+/// Coverage for PE-header architecture parsing.
+/// The validator reads the DOS MZ stub, follows the PE
/// header offset at 0x3c, checks the PE\0\0 signature, and compares the
/// machine field against the required .
///
@@ -129,7 +129,7 @@ public sealed class WorkerExecutableValidatorTests : IDisposable
return path;
}
- ///
+ /// Deletes the temporary PE fixture files created by this test class, best-effort.
public void Dispose()
{
foreach (string path in _tempFiles)
diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Workers/WorkerFrameProtocolTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Workers/WorkerFrameProtocolTests.cs
index 6696ac6..ed26f6d 100644
--- a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Workers/WorkerFrameProtocolTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Workers/WorkerFrameProtocolTests.cs
@@ -11,6 +11,7 @@ public sealed class WorkerFrameProtocolTests
private const string SessionId = "session-1";
/// Verifies that writing and reading a valid envelope round-trips the frame correctly.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task WriteAndReadAsync_WithValidEnvelope_RoundTripsFrame()
{
@@ -29,6 +30,7 @@ public sealed class WorkerFrameProtocolTests
}
/// Verifies that reading a frame with partial reads reassembles the frame correctly.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task ReadAsync_WithPartialReads_ReassemblesFrame()
{
@@ -45,6 +47,7 @@ public sealed class WorkerFrameProtocolTests
}
/// Verifies that reading a frame with zero length throws a malformed length exception.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task ReadAsync_WithZeroLengthFrame_ThrowsMalformedLength()
{
@@ -60,6 +63,7 @@ public sealed class WorkerFrameProtocolTests
}
/// Verifies that reading a frame with oversized length throws before allocating the payload.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task ReadAsync_WithOversizedLength_ThrowsBeforePayloadAllocation()
{
@@ -77,6 +81,7 @@ public sealed class WorkerFrameProtocolTests
}
/// Verifies that reading a frame with wrong protocol version throws a protocol version mismatch exception.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task ReadAsync_WithWrongProtocolVersion_ThrowsProtocolVersionMismatch()
{
@@ -94,6 +99,7 @@ public sealed class WorkerFrameProtocolTests
}
/// Verifies that reading a frame with wrong session ID throws a session mismatch exception.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task ReadAsync_WithWrongSessionId_ThrowsSessionMismatch()
{
@@ -111,6 +117,7 @@ public sealed class WorkerFrameProtocolTests
}
/// Verifies that reading a frame with malformed payload throws an invalid envelope exception.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task ReadAsync_WithMalformedPayload_ThrowsInvalidEnvelope()
{
@@ -127,6 +134,7 @@ public sealed class WorkerFrameProtocolTests
}
/// Verifies that reading a frame with missing envelope body throws an invalid envelope exception.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task ReadAsync_WithMissingEnvelopeBody_ThrowsInvalidEnvelope()
{
@@ -144,6 +152,7 @@ public sealed class WorkerFrameProtocolTests
}
/// Verifies that writing an oversized envelope throws a message too large exception.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task WriteAsync_WithOversizedEnvelope_ThrowsMessageTooLarge()
{
diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Workers/WorkerProcessLauncherTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Workers/WorkerProcessLauncherTests.cs
index 4c1c4b2..f9d0e43 100644
--- a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Workers/WorkerProcessLauncherTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Workers/WorkerProcessLauncherTests.cs
@@ -15,6 +15,7 @@ public sealed class WorkerProcessLauncherTests
private const string Nonce = "super-secret-nonce";
/// Verifies that a valid worker executable starts with correct bootstrap arguments and nonce environment variable.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task LaunchAsync_WithValidWorker_StartsProcessWithBootstrapArgumentsAndNonceEnvironment()
{
@@ -49,6 +50,7 @@ public sealed class WorkerProcessLauncherTests
}
/// Verifies that a failed startup probe kills and disposes the worker process.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task LaunchAsync_WhenStartupProbeFails_KillsAndDisposesWorker()
{
@@ -76,6 +78,7 @@ public sealed class WorkerProcessLauncherTests
}
/// Verifies that transient startup probe failures are retried without respawning the worker process.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task LaunchAsync_WhenStartupProbeFailsTransiently_RetriesWithoutRespawningWorker()
{
@@ -103,6 +106,7 @@ public sealed class WorkerProcessLauncherTests
}
/// Verifies that a startup probe timeout kills and disposes the worker process.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task LaunchAsync_WhenStartupTimesOut_KillsAndDisposesWorker()
{
@@ -129,6 +133,7 @@ public sealed class WorkerProcessLauncherTests
}
/// Verifies that a missing worker executable fails before attempting to start the process.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task LaunchAsync_WhenExecutableDoesNotExist_FailsBeforeStartingProcess()
{
@@ -146,6 +151,7 @@ public sealed class WorkerProcessLauncherTests
}
/// Verifies that a worker executable with mismatched architecture fails before attempting to start.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task LaunchAsync_WhenExecutableArchitectureDoesNotMatch_FailsBeforeStartingProcess()
{
@@ -163,6 +169,7 @@ public sealed class WorkerProcessLauncherTests
}
/// Verifies that a worker that has already exited fails and disposes without additional killing.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task LaunchAsync_WhenWorkerAlreadyExited_FailsAndDisposesWorkerWithoutKill()
{
@@ -308,7 +315,7 @@ public sealed class WorkerProcessLauncherTests
/// Gets a value indicating whether the Dispose method was called.
public bool DisposeCalled { get; private set; }
- ///
+ /// Records that the pipe reservation was released.
public void Dispose()
{
DisposeCalled = true;
@@ -327,6 +334,7 @@ public sealed class WorkerProcessLauncherTests
public string Path { get; }
/// Creates a new temporary directory for testing.
+ /// The created wrapper.
public static TestDirectory Create()
{
string path = System.IO.Path.Combine(System.IO.Path.GetTempPath(), $"mxgateway-tests-{Guid.NewGuid():N}");
@@ -355,7 +363,7 @@ public sealed class WorkerProcessLauncherTests
return path;
}
- ///
+ /// Deletes the temporary test directory and its contents.
public void Dispose()
{
Directory.Delete(Path, recursive: true);
diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Metrics/GatewayMetricsTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Metrics/GatewayMetricsTests.cs
index 3fa974c..0181f5e 100644
--- a/src/ZB.MOM.WW.MxGateway.Tests/Metrics/GatewayMetricsTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Tests/Metrics/GatewayMetricsTests.cs
@@ -68,7 +68,7 @@ public sealed class GatewayMetricsTests
/// Verifies that increments
/// mxgateway.alarms.provider_switches by one with the expected from/to/reason tags.
/// The listener filters by the specific instance
- /// to avoid cross-talk between parallel tests (Tests-027).
+ /// to avoid cross-talk between parallel tests.
///
[Fact]
public void AlarmProviderSwitched_IncrementsCounterWithExpectedTags()
diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Security/Audit/CanonicalAuditStoreAndAdapterTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Security/Audit/CanonicalAuditStoreAndAdapterTests.cs
index 2eefb63..761d645 100644
--- a/src/ZB.MOM.WW.MxGateway.Tests/Security/Audit/CanonicalAuditStoreAndAdapterTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Tests/Security/Audit/CanonicalAuditStoreAndAdapterTests.cs
@@ -8,7 +8,7 @@ using ZB.MOM.WW.MxGateway.Tests.Security.Authentication;
namespace ZB.MOM.WW.MxGateway.Tests.Security.Audit;
///
-/// Tests the Task 2.3 canonical audit plumbing: the gateway-owned
+/// Tests the canonical audit plumbing: the gateway-owned
/// (round-trips canonical
/// s through the new audit_event table), the best-effort
/// , and the
@@ -21,6 +21,7 @@ public sealed class CanonicalAuditStoreAndAdapterTests : IDisposable
private readonly List _tempDirectories = [];
/// A canonical event with all fields populated round-trips through the store.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task Store_InsertThenListRecent_RoundTripsAllFields()
{
@@ -61,6 +62,7 @@ public sealed class CanonicalAuditStoreAndAdapterTests : IDisposable
}
/// Nullable canonical fields round-trip as null, not empty string.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task Store_InsertWithNullOptionalFields_RoundTripsAsNull()
{
@@ -86,6 +88,7 @@ public sealed class CanonicalAuditStoreAndAdapterTests : IDisposable
}
/// ListRecent returns newest-first.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task Store_ListRecent_ReturnsNewestFirst()
{
@@ -100,6 +103,7 @@ public sealed class CanonicalAuditStoreAndAdapterTests : IDisposable
}
/// The writer is best-effort: a faulting store does not surface to the caller.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task Writer_WhenStoreFails_DoesNotThrow()
{
@@ -123,6 +127,7 @@ public sealed class CanonicalAuditStoreAndAdapterTests : IDisposable
/// A library event with a KeyId maps to a canonical Success event under category ApiKey,
/// and the adapter maps it back to the original entry for the dashboard view.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task Adapter_KeyedEvent_RoundTripsThroughCanonicalStore()
{
@@ -158,6 +163,7 @@ public sealed class CanonicalAuditStoreAndAdapterTests : IDisposable
}
/// The keyless library init-db event maps to Actor "system" and back to a null KeyId.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task Adapter_InitDbKeylessEvent_MapsToSystemActor()
{
@@ -184,6 +190,7 @@ public sealed class CanonicalAuditStoreAndAdapterTests : IDisposable
}
/// Any other keyless library event maps to Actor "cli" and back to a null KeyId.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task Adapter_OtherKeylessEvent_MapsToCliActor()
{
@@ -207,6 +214,7 @@ public sealed class CanonicalAuditStoreAndAdapterTests : IDisposable
}
/// A constraint-denied library event maps to Outcome.Denied.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task Adapter_ConstraintDeniedEvent_MapsToDeniedOutcome()
{
@@ -234,6 +242,7 @@ public sealed class CanonicalAuditStoreAndAdapterTests : IDisposable
/// The adapter does NOT throw when the underlying write fails (it forwards through the
/// best-effort writer), preserving the IApiKeyAuditStore caller's flow.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task Adapter_WhenWriterFails_DoesNotThrow()
{
diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Security/Authentication/ApiKeyAdminCliRunnerTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Security/Authentication/ApiKeyAdminCliRunnerTests.cs
index c6cce0f..e95d5cc 100644
--- a/src/ZB.MOM.WW.MxGateway.Tests/Security/Authentication/ApiKeyAdminCliRunnerTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Tests/Security/Authentication/ApiKeyAdminCliRunnerTests.cs
@@ -14,6 +14,7 @@ public sealed class ApiKeyAdminCliRunnerTests : IDisposable
{
private readonly List _tempDirectories = [];
/// Verifies that CreateKeyAsync creates an authenticating key and audits the action.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task CreateKeyAsync_CreatesAuthenticatingKeyAndAudits()
{
@@ -52,6 +53,7 @@ public sealed class ApiKeyAdminCliRunnerTests : IDisposable
}
/// Verifies that ListKeysAsync does not print the raw secret.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task ListKeysAsync_DoesNotPrintRawSecret()
{
@@ -82,6 +84,7 @@ public sealed class ApiKeyAdminCliRunnerTests : IDisposable
}
/// Verifies that RevokeKeyAsync causes the revoked key to fail verification and is audited.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task RevokeKeyAsync_RevokedKeyFailsVerificationAndAudits()
{
@@ -117,6 +120,7 @@ public sealed class ApiKeyAdminCliRunnerTests : IDisposable
}
/// Verifies that RotateKeyAsync prints the new secret once and invalidates the old secret.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task RotateKeyAsync_PrintsNewSecretOnceAndInvalidatesOldSecret()
{
@@ -154,6 +158,7 @@ public sealed class ApiKeyAdminCliRunnerTests : IDisposable
}
/// Verifies that CreateKeyAsync prints the raw secret exactly once.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task CreateKeyAsync_PrintsRawSecretExactlyOnce()
{
@@ -182,6 +187,7 @@ public sealed class ApiKeyAdminCliRunnerTests : IDisposable
}
/// Verifies that API key constraints are persisted correctly.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task CreateKeyAsync_WithConstraints_PersistsConstraints()
{
diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Security/Authentication/ApiKeyAdminCommandLineParserTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Security/Authentication/ApiKeyAdminCommandLineParserTests.cs
index a7b5c27..3f82499 100644
--- a/src/ZB.MOM.WW.MxGateway.Tests/Security/Authentication/ApiKeyAdminCommandLineParserTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Tests/Security/Authentication/ApiKeyAdminCommandLineParserTests.cs
@@ -53,7 +53,7 @@ public sealed class ApiKeyAdminCommandLineParserTests
}
///
- /// Server-004 regression: a create-key command with a non-canonical scope
+ /// A create-key command with a non-canonical scope
/// string (e.g. CLAUDE.md's stale invoke instead of invoke:read)
/// must be rejected at parse time rather than silently persisting an
/// unusable scope the authorization resolver never matches.
diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Security/Authentication/ApiKeyVerifierTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Security/Authentication/ApiKeyVerifierTests.cs
index 5fa6f0e..935dc7d 100644
--- a/src/ZB.MOM.WW.MxGateway.Tests/Security/Authentication/ApiKeyVerifierTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Tests/Security/Authentication/ApiKeyVerifierTests.cs
@@ -18,6 +18,7 @@ public sealed class ApiKeyVerifierTests
private static readonly ApiKeyOptions Options = new() { TokenPrefix = "mxgw" };
/// Verifies that VerifyAsync returns identity and scopes for a valid key.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task VerifyAsync_ValidKey_ReturnsIdentityAndScopes()
{
@@ -38,6 +39,7 @@ public sealed class ApiKeyVerifierTests
}
/// Verifies that VerifyAsync does not expose the raw secret in the result.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task VerifyAsync_ValidKey_DoesNotExposeRawSecretInResult()
{
@@ -55,6 +57,7 @@ public sealed class ApiKeyVerifierTests
/// Verifies that VerifyAsync fails as missing/malformed for a malformed key.
/// Authorization header value to test.
+ /// A task that represents the asynchronous operation.
[Theory]
[InlineData("")]
[InlineData("Bearer mxgw_operator01")]
@@ -72,6 +75,7 @@ public sealed class ApiKeyVerifierTests
}
/// Verifies that VerifyAsync fails for an unknown key.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task VerifyAsync_UnknownKey_Fails()
{
@@ -86,6 +90,7 @@ public sealed class ApiKeyVerifierTests
}
/// Verifies that VerifyAsync fails for a wrong secret (constant-time compare rejects it).
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task VerifyAsync_WrongSecret_Fails()
{
@@ -102,6 +107,7 @@ public sealed class ApiKeyVerifierTests
}
/// Verifies that VerifyAsync fails for a revoked key.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task VerifyAsync_RevokedKey_Fails()
{
@@ -118,6 +124,7 @@ public sealed class ApiKeyVerifierTests
}
/// Verifies that VerifyAsync fails closed when the pepper is missing.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task VerifyAsync_MissingPepper_Fails()
{
@@ -160,6 +167,7 @@ public sealed class ApiKeyVerifierTests
private sealed class FakePepperProvider(string? pepper) : IApiKeyPepperProvider
{
/// Returns the configured pepper (or null to simulate an unavailable pepper).
+ /// The configured pepper, or null to simulate an unavailable pepper.
public string? GetPepper() => pepper;
}
@@ -169,13 +177,19 @@ public sealed class ApiKeyVerifierTests
/// Gets whether the key was marked as used.
public bool MarkedUsed { get; private set; }
- ///
+ /// Returns the fake's single stored key if its ID matches, regardless of revocation.
+ /// Key ID to look up.
+ /// Cancellation token.
+ /// The matching stored key record, or null if no key matches.
public Task FindByKeyIdAsync(string keyId, CancellationToken ct)
{
return Task.FromResult(storedKey?.KeyId == keyId ? storedKey : null);
}
- ///
+ /// Returns the fake's single stored key if its ID matches and it has not been revoked.
+ /// Key ID to look up.
+ /// Cancellation token.
+ /// The matching, non-revoked stored key record, or null otherwise.
public Task FindActiveByKeyIdAsync(string keyId, CancellationToken ct)
{
return Task.FromResult(
@@ -184,7 +198,11 @@ public sealed class ApiKeyVerifierTests
: null);
}
- ///
+ /// Records that the stored key was used, exposed via for assertions.
+ /// Key ID that was used.
+ /// Timestamp of use (unused by the fake).
+ /// Cancellation token.
+ /// A task that represents the asynchronous operation.
public Task MarkUsedAsync(string keyId, DateTimeOffset whenUtc, CancellationToken ct)
{
MarkedUsed = storedKey?.KeyId == keyId;
diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Security/Authentication/SqliteAuthStoreTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Security/Authentication/SqliteAuthStoreTests.cs
index 8b6b633..9e7fb7b 100644
--- a/src/ZB.MOM.WW.MxGateway.Tests/Security/Authentication/SqliteAuthStoreTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Tests/Security/Authentication/SqliteAuthStoreTests.cs
@@ -23,6 +23,7 @@ public sealed class SqliteAuthStoreTests : IDisposable
///
/// Verifies that MigrateAsync initializes the database schema at the donor's version (2).
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task MigrateAsync_EmptyDatabase_InitializesCurrentSchema()
{
@@ -41,6 +42,7 @@ public sealed class SqliteAuthStoreTests : IDisposable
///
/// Verifies that MigrateAsync migrates and is idempotent.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task MigrateAsync_ExistingVersionZeroDatabase_MigratesIdempotently()
{
@@ -61,6 +63,7 @@ public sealed class SqliteAuthStoreTests : IDisposable
///
/// Verifies that gateway startup fails with a newer schema version.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task StartAsync_NewerSchemaVersion_BlocksStartup()
{
@@ -83,6 +86,7 @@ public sealed class SqliteAuthStoreTests : IDisposable
/// Verifies that FindActiveByKeyIdAsync returns an active key, reading a row whose columns match
/// the donor schema (peppered secret_hash BLOB, ordinal-sorted scopes JSON).
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task FindActiveByKeyIdAsync_ExistingActiveKey_ReturnsKey()
{
@@ -106,6 +110,7 @@ public sealed class SqliteAuthStoreTests : IDisposable
///
/// Verifies that FindActiveByKeyIdAsync returns null for a revoked key.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task FindActiveByKeyIdAsync_RevokedKey_ReturnsNull()
{
@@ -129,6 +134,7 @@ public sealed class SqliteAuthStoreTests : IDisposable
///
/// Verifies that the audit store persists audit events.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task ApiKeyAuditStore_AppendAsync_PersistsAuditEvent()
{
@@ -163,6 +169,7 @@ public sealed class SqliteAuthStoreTests : IDisposable
/// the auth database in WAL journal mode so concurrent readers and writers degrade
/// gracefully instead of surfacing SQLITE_BUSY on the request path.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task OpenConnectionAsync_EnablesWalJournalModeAndBusyTimeout()
{
diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Security/Authentication/TempDatabaseDirectory.cs b/src/ZB.MOM.WW.MxGateway.Tests/Security/Authentication/TempDatabaseDirectory.cs
index ff97cab..9b3562f 100644
--- a/src/ZB.MOM.WW.MxGateway.Tests/Security/Authentication/TempDatabaseDirectory.cs
+++ b/src/ZB.MOM.WW.MxGateway.Tests/Security/Authentication/TempDatabaseDirectory.cs
@@ -22,6 +22,7 @@ internal sealed class TempDatabaseDirectory : IDisposable
/// Creates a new uniquely named temporary directory under the given prefix.
/// Folder name placed under %TEMP% to group related test directories.
+ /// The created temporary directory handle.
public static TempDatabaseDirectory Create(string prefix)
{
string path = System.IO.Path.Combine(
@@ -35,12 +36,13 @@ internal sealed class TempDatabaseDirectory : IDisposable
/// Returns a database file path inside this temporary directory.
/// Database file name; defaults to the gateway auth database name.
+ /// The full path to the database file.
public string DatabasePath(string fileName = "gateway-auth.db")
{
return System.IO.Path.Combine(Path, fileName);
}
- ///
+ /// Clears pooled SQLite connections and deletes the temporary directory.
public void Dispose()
{
if (_disposed)
diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Security/Authorization/ConstraintEnforcerTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Security/Authorization/ConstraintEnforcerTests.cs
index 6d01ed4..707602e 100644
--- a/src/ZB.MOM.WW.MxGateway.Tests/Security/Authorization/ConstraintEnforcerTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Tests/Security/Authorization/ConstraintEnforcerTests.cs
@@ -16,6 +16,7 @@ namespace ZB.MOM.WW.MxGateway.Tests.Security.Authorization;
public sealed class ConstraintEnforcerTests
{
/// Verifies that read outside allowed subtree returns failure.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task CheckReadTagAsync_WhenOutsideReadSubtree_ReturnsFailure()
{
@@ -35,6 +36,7 @@ public sealed class ConstraintEnforcerTests
}
/// Verifies that write with high classification returns failure and audits.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task CheckWriteHandleAsync_WhenClassificationTooHigh_ReturnsFailureAndAudits()
{
@@ -84,6 +86,7 @@ public sealed class ConstraintEnforcerTests
}
/// A denial carrying a parseable correlation id stores it on the audit record.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task RecordDenialAsync_WithGuidCorrelationId_StoresCorrelationId()
{
@@ -106,6 +109,7 @@ public sealed class ConstraintEnforcerTests
/// A denial with a non-GUID correlation id leaves the typed audit correlation id null but
/// still preserves the raw client correlation id in DetailsJson so it is not lost.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task RecordDenialAsync_WithNonGuidCorrelationId_LeavesCorrelationIdNullButPreservesRawInDetails()
{
@@ -130,6 +134,7 @@ public sealed class ConstraintEnforcerTests
}
/// A denial with no identity records the canonical "anonymous" actor.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task RecordDenialAsync_WithoutIdentity_UsesAnonymousActor()
{
@@ -150,6 +155,7 @@ public sealed class ConstraintEnforcerTests
}
/// Verifies that historized-only constraint requires historized attribute.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task CheckReadTagAsync_WithHistorizedOnly_RequiresRequestedAttributeToBeHistorized()
{
@@ -169,6 +175,7 @@ public sealed class ConstraintEnforcerTests
}
/// Verifies that alarm-only constraint requires alarm attribute.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task CheckReadTagAsync_WithAlarmOnly_RequiresRequestedAttributeToBeAlarm()
{
@@ -188,6 +195,7 @@ public sealed class ConstraintEnforcerTests
}
/// Verifies that attribute-only constraint fails closed for object tag.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task CheckReadTagAsync_WithAttributeOnlyConstraint_FailsClosedForObjectTag()
{
@@ -212,6 +220,7 @@ public sealed class ConstraintEnforcerTests
/// Without the [] fallback in ResolveTarget the bare name misses the index and a
/// read-constrained key gets a spurious tag_metadata denial for an AddItem it should allow.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task CheckReadTagAsync_WithBareArrayName_ResolvesViaArraySuffixFallback()
{
@@ -236,6 +245,7 @@ public sealed class ConstraintEnforcerTests
/// A bare non-array name that is genuinely absent from the index still resolves to null:
/// the [] probe must not manufacture a false positive for a scalar/missing tag.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task CheckReadTagAsync_WithMissingNonArrayName_StillFailsToResolve()
{
@@ -265,8 +275,9 @@ public sealed class ConstraintEnforcerTests
///
/// The []-suffix fallback widened *resolution* of a bare array name, not *authorization*:
/// a bare array attribute that resolves through the fallback but is outside the key's read scope
- /// must still be denied with a read_scope failure (Server-058).
+ /// must still be denied with a read_scope failure.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task CheckReadTagAsync_WithBareArrayName_OutOfScope_StillDeniedReadScope()
{
@@ -289,8 +300,9 @@ public sealed class ConstraintEnforcerTests
///
/// A write against an array handle whose registration carries the suffixed form ("Pump_001.Levels[]")
/// resolves through ResolveTarget and is denied with a write_scope failure when the
- /// array attribute is outside the key's write scope (Server-058).
+ /// array attribute is outside the key's write scope.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task CheckWriteHandleAsync_WithSuffixedArrayRegistration_OutOfScope_StillDeniedWriteScope()
{
@@ -331,9 +343,9 @@ public sealed class ConstraintEnforcerTests
///
/// A write against an in-scope array handle whose registration carries the suffixed form is still
/// denied when the array attribute's SecurityClassification exceeds the key's
- /// MaxWriteClassification, resolved through ResolveTarget via the suffixed address
- /// (Server-058).
+ /// MaxWriteClassification, resolved through ResolveTarget via the suffixed address.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task CheckWriteHandleAsync_WithSuffixedArrayRegistration_ClassificationTooHigh_StillDenied()
{
@@ -490,10 +502,14 @@ public sealed class ConstraintEnforcerTests
/// Gets the current cache entry.
public GalaxyHierarchyCacheEntry Current { get; } = current;
- ///
+ /// No-op refresh; the stub's entry never changes.
+ /// Token to cancel the asynchronous operation.
+ /// A task that represents the asynchronous operation.
public Task RefreshAsync(CancellationToken cancellationToken) => Task.CompletedTask;
- ///
+ /// No-op wait; the stub's entry is already loaded.
+ /// Token to cancel the asynchronous operation.
+ /// A task that represents the asynchronous operation.
public Task WaitForFirstLoadAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
@@ -502,7 +518,10 @@ public sealed class ConstraintEnforcerTests
/// Gets the recorded canonical audit events.
public List Events { get; } = [];
- ///
+ /// Records the audit event in for later assertions.
+ /// The audit event to record.
+ /// Token to cancel the asynchronous operation.
+ /// A task that represents the asynchronous operation.
public Task WriteAsync(AuditEvent auditEvent, CancellationToken cancellationToken = default)
{
Events.Add(auditEvent);
diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Security/Authorization/GatewayGrpcAuthorizationInterceptorTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Security/Authorization/GatewayGrpcAuthorizationInterceptorTests.cs
index 5307034..f1dc883 100644
--- a/src/ZB.MOM.WW.MxGateway.Tests/Security/Authorization/GatewayGrpcAuthorizationInterceptorTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Tests/Security/Authorization/GatewayGrpcAuthorizationInterceptorTests.cs
@@ -23,6 +23,7 @@ namespace ZB.MOM.WW.MxGateway.Tests.Security.Authorization;
public sealed class GatewayGrpcAuthorizationInterceptorTests
{
/// Verifies that missing API key returns unauthenticated status.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task UnaryServerHandler_MissingApiKey_ReturnsUnauthenticated()
{
@@ -41,6 +42,7 @@ public sealed class GatewayGrpcAuthorizationInterceptorTests
}
/// Verifies that invalid API key error does not expose raw credentials.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task UnaryServerHandler_InvalidApiKey_DoesNotExposeRawCredentialInStatus()
{
@@ -59,6 +61,7 @@ public sealed class GatewayGrpcAuthorizationInterceptorTests
}
/// Verifies that valid key without required scope returns permission denied.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task UnaryServerHandler_ValidApiKeyMissingScope_ReturnsPermissionDenied()
{
@@ -77,6 +80,7 @@ public sealed class GatewayGrpcAuthorizationInterceptorTests
}
/// Verifies that valid key with scope sets request identity for the handler.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task UnaryServerHandler_ValidApiKeyWithScope_SetsRequestIdentity()
{
@@ -103,6 +107,7 @@ public sealed class GatewayGrpcAuthorizationInterceptorTests
}
/// Verifies that server stream handler requires proper scope.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task ServerStreamingServerHandler_ValidApiKeyMissingScope_ReturnsPermissionDenied()
{
@@ -122,6 +127,7 @@ public sealed class GatewayGrpcAuthorizationInterceptorTests
}
/// Verifies that server stream handler allows streams with proper scope.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task ServerStreamingServerHandler_ValidApiKeyWithScope_AllowsStream()
{
@@ -147,6 +153,7 @@ public sealed class GatewayGrpcAuthorizationInterceptorTests
}
/// Verifies that disabled authentication skips API key verification.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task UnaryServerHandler_AuthenticationDisabled_SkipsApiKeyVerification()
{
@@ -173,6 +180,7 @@ public sealed class GatewayGrpcAuthorizationInterceptorTests
/// that lacks the session:open scope, and asserts the interceptor denies the
/// call with before the service runs.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task InterceptorComposedWithService_OpenSessionMissingScope_DeniesBeforeServiceRuns()
{
@@ -200,6 +208,7 @@ public sealed class GatewayGrpcAuthorizationInterceptorTests
/// that holds session:open, and asserts the service runs and observes the
/// interceptor-supplied identity.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task InterceptorComposedWithService_OpenSessionWithScope_RunsServiceWithIdentity()
{
@@ -226,6 +235,7 @@ public sealed class GatewayGrpcAuthorizationInterceptorTests
/// because the wrapped command is a write, confirming command-scope mapping is
/// enforced through the full composition.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task InterceptorComposedWithService_InvokeWriteCommandWithReadScope_DeniesBeforeServiceRuns()
{
@@ -261,6 +271,7 @@ public sealed class GatewayGrpcAuthorizationInterceptorTests
/// . Ack is a write-shaped mutation against
/// alarm state, so it carries the same scope as MxCommandKind.Write.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task UnaryServerHandler_AcknowledgeAlarmMissingScope_ReturnsPermissionDenied()
{
@@ -279,6 +290,7 @@ public sealed class GatewayGrpcAuthorizationInterceptorTests
}
/// Verifies that an API key holding invoke:write may call AcknowledgeAlarm.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task UnaryServerHandler_AcknowledgeAlarmWithScope_RunsHandler()
{
@@ -305,6 +317,7 @@ public sealed class GatewayGrpcAuthorizationInterceptorTests
/// lack . Active-alarm snapshots are part of the
/// alarm/event surface and share the same scope as StreamEvents.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task ServerStreamingServerHandler_QueryActiveAlarmsMissingScope_ReturnsPermissionDenied()
{
@@ -324,6 +337,7 @@ public sealed class GatewayGrpcAuthorizationInterceptorTests
}
/// Verifies that an API key holding events:read may call QueryActiveAlarms.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task ServerStreamingServerHandler_QueryActiveAlarmsWithScope_RunsHandler()
{
diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Security/Tls/KestrelTlsInspectorTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Security/Tls/KestrelTlsInspectorTests.cs
index 4851090..469a697 100644
--- a/src/ZB.MOM.WW.MxGateway.Tests/Security/Tls/KestrelTlsInspectorTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Tests/Security/Tls/KestrelTlsInspectorTests.cs
@@ -11,16 +11,19 @@ public sealed class KestrelTlsInspectorTests
.AddInMemoryCollection(entries.ToDictionary(e => e.Key, e => (string?)e.Value))
.Build();
+ /// An HTTPS endpoint with no certificate configured requires a generated certificate.
[Fact]
public void RequiresGeneratedCertificate_True_WhenHttpsEndpointHasNoCertificate()
=> Assert.True(KestrelTlsInspector.RequiresGeneratedCertificate(
Config(("Kestrel:Endpoints:Http:Url", "https://0.0.0.0:5120"))));
+ /// When every configured endpoint is plaintext, no generated certificate is required.
[Fact]
public void RequiresGeneratedCertificate_False_WhenAllEndpointsPlaintext()
=> Assert.False(KestrelTlsInspector.RequiresGeneratedCertificate(
Config(("Kestrel:Endpoints:Http:Url", "http://0.0.0.0:5120"))));
+ /// An HTTPS endpoint with its own certificate path configured does not require a generated certificate.
[Fact]
public void RequiresGeneratedCertificate_False_WhenHttpsEndpointHasOwnCertificate()
=> Assert.False(KestrelTlsInspector.RequiresGeneratedCertificate(
@@ -28,10 +31,12 @@ public sealed class KestrelTlsInspectorTests
("Kestrel:Endpoints:Http:Url", "https://0.0.0.0:5120"),
("Kestrel:Endpoints:Http:Certificate:Path", @"C:\certs\real.pfx"))));
+ /// With no endpoints configured at all, no generated certificate is required.
[Fact]
public void RequiresGeneratedCertificate_False_WhenNoEndpointsConfigured()
=> Assert.False(KestrelTlsInspector.RequiresGeneratedCertificate(Config()));
+ /// An HTTPS endpoint configured with only a certificate thumbprint does not require a generated certificate.
[Fact]
public void RequiresGeneratedCertificate_False_WhenHttpsEndpointHasThumbprintOnly()
=> Assert.False(KestrelTlsInspector.RequiresGeneratedCertificate(
@@ -39,6 +44,7 @@ public sealed class KestrelTlsInspectorTests
("Kestrel:Endpoints:Https:Url", "https://0.0.0.0:5120"),
("Kestrel:Endpoints:Https:Certificate:Thumbprint", "AABBCCDDEEFF00112233445566778899AABBCCDD"))));
+ /// An HTTPS endpoint configured with only a certificate subject does not require a generated certificate.
[Fact]
public void RequiresGeneratedCertificate_False_WhenHttpsEndpointHasSubjectOnly()
=> Assert.False(KestrelTlsInspector.RequiresGeneratedCertificate(
@@ -46,11 +52,13 @@ public sealed class KestrelTlsInspectorTests
("Kestrel:Endpoints:Https:Url", "https://0.0.0.0:5120"),
("Kestrel:Endpoints:Https:Certificate:Subject", "CN=myserver"))));
+ /// An uppercase HTTPS URL scheme is still recognized as HTTPS and requires a generated certificate.
[Fact]
public void RequiresGeneratedCertificate_True_WhenHttpsUrlIsUppercase()
=> Assert.True(KestrelTlsInspector.RequiresGeneratedCertificate(
Config(("Kestrel:Endpoints:Https:Url", "HTTPS://0.0.0.0:5120"))));
+ /// An HTTPS endpoint covered by the Kestrel default certificate does not require a generated certificate.
[Fact]
public void RequiresGeneratedCertificate_False_WhenKestrelDefaultCertificateConfigured()
=> Assert.False(KestrelTlsInspector.RequiresGeneratedCertificate(
@@ -58,6 +66,7 @@ public sealed class KestrelTlsInspectorTests
("Kestrel:Endpoints:Https:Url", "https://0.0.0.0:5120"),
("Kestrel:Certificates:Default:Path", @"C:\certs\default.pfx"))));
+ /// Among mixed endpoints, one HTTPS endpoint without a certificate still requires a generated certificate.
[Fact]
public void RequiresGeneratedCertificate_True_WhenMixedEndpointsAndOneHttpsHasNoCert()
=> Assert.True(KestrelTlsInspector.RequiresGeneratedCertificate(
diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Security/Tls/SelfSignedCertificateProviderTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Security/Tls/SelfSignedCertificateProviderTests.cs
index 7b8dfc8..1a4cd69 100644
--- a/src/ZB.MOM.WW.MxGateway.Tests/Security/Tls/SelfSignedCertificateProviderTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Tests/Security/Tls/SelfSignedCertificateProviderTests.cs
@@ -12,6 +12,7 @@ public sealed class SelfSignedCertificateProviderTests
private static SelfSignedCertificateProvider CreateProvider(TlsOptions options, FakeTimeProvider time)
=> new(options, NullLogger.Instance, time);
+ /// Verifies that a generated certificate has the expected validity window, SANs (localhost, machine name, additional DNS names, loopback IPs), and the serverAuth EKU.
[Fact]
public void GenerateCertificate_HasExpectedSansEkuAndValidity()
{
@@ -39,6 +40,7 @@ public sealed class SelfSignedCertificateProviderTests
o => o.Value == "1.3.6.1.5.5.7.3.1"); // serverAuth
}
+ /// Verifies that LoadOrCreate generates and persists a certificate on first call, then reuses the same persisted certificate (same thumbprint) on a subsequent call.
[Fact]
public void LoadOrCreate_GeneratesPersistsAndReuses_SameThumbprint()
{
@@ -58,6 +60,7 @@ public sealed class SelfSignedCertificateProviderTests
finally { Directory.Delete(dir, recursive: true); }
}
+ /// Verifies that LoadOrCreate regenerates the certificate (a new thumbprint) once the persisted certificate's validity window has elapsed.
[Fact]
public void LoadOrCreate_Regenerates_WhenPersistedCertExpired()
{
@@ -77,6 +80,7 @@ public sealed class SelfSignedCertificateProviderTests
finally { Directory.Delete(dir, recursive: true); }
}
+ /// Verifies that LoadOrCreate regenerates a valid certificate when the persisted PFX file is corrupt or unreadable.
[Fact]
public void LoadOrCreate_Regenerates_WhenPersistedFileCorrupt()
{
@@ -92,6 +96,7 @@ public sealed class SelfSignedCertificateProviderTests
finally { Directory.Delete(dir, recursive: true); }
}
+ /// Verifies that LoadOrCreate throws for an expired persisted certificate when regeneration is disabled.
[Fact]
public void LoadOrCreate_Throws_WhenExpiredAndRegenerateDisabled()
{
@@ -108,6 +113,7 @@ public sealed class SelfSignedCertificateProviderTests
finally { Directory.Delete(dir, recursive: true); }
}
+ /// Verifies that LoadOrCreate throws when SelfSignedCertPath is blank.
[Fact]
public void LoadOrCreate_Throws_WhenSelfSignedCertPathBlank()
{
diff --git a/src/ZB.MOM.WW.MxGateway.Tests/TestSupport/FakeWorkerProcess.cs b/src/ZB.MOM.WW.MxGateway.Tests/TestSupport/FakeWorkerProcess.cs
index 2bb0386..10a5e9c 100644
--- a/src/ZB.MOM.WW.MxGateway.Tests/TestSupport/FakeWorkerProcess.cs
+++ b/src/ZB.MOM.WW.MxGateway.Tests/TestSupport/FakeWorkerProcess.cs
@@ -23,13 +23,13 @@ public sealed class FakeWorkerProcess(int processId) : IWorkerProcess
{
private readonly TaskCompletionSource _exited = new(TaskCreationOptions.RunContinuationsAsynchronously);
- /// Gets the process identifier.
+ ///
public int Id { get; } = processId;
- /// Gets or sets a value indicating whether the process has exited.
+ ///
public bool HasExited { get; set; }
- /// Gets or sets the exit code of the process, or if it has not exited.
+ ///
public int? ExitCode { get; set; }
/// Gets the number of times was called.
@@ -62,7 +62,7 @@ public sealed class FakeWorkerProcess(int processId) : IWorkerProcess
MarkExited(-1);
}
- ///
+ /// Marks this fake as disposed so tests can assert was called.
public void Dispose() => IsDisposed = true;
///
diff --git a/src/ZB.MOM.WW.MxGateway.Tests/TestSupport/NullDashboardEventBroadcaster.cs b/src/ZB.MOM.WW.MxGateway.Tests/TestSupport/NullDashboardEventBroadcaster.cs
index b7d9a34..ffe6097 100644
--- a/src/ZB.MOM.WW.MxGateway.Tests/TestSupport/NullDashboardEventBroadcaster.cs
+++ b/src/ZB.MOM.WW.MxGateway.Tests/TestSupport/NullDashboardEventBroadcaster.cs
@@ -7,7 +7,7 @@ namespace ZB.MOM.WW.MxGateway.Tests.TestSupport;
/// that drops every event on the floor.
/// Used by tests that need to wire up EventStreamService but do not care
/// about the dashboard fan-out side-channel. The singleton
-/// mirrors the Tests-007 / Tests-021 shared-fake pattern under
+/// mirrors the shared-fake pattern under
/// src/ZB.MOM.WW.MxGateway.Tests/TestSupport/.
///
public sealed class NullDashboardEventBroadcaster : IDashboardEventBroadcaster
diff --git a/src/ZB.MOM.WW.MxGateway.Tests/TestSupport/RecordingDashboardEventBroadcaster.cs b/src/ZB.MOM.WW.MxGateway.Tests/TestSupport/RecordingDashboardEventBroadcaster.cs
index 5e687a7..a3a5f92 100644
--- a/src/ZB.MOM.WW.MxGateway.Tests/TestSupport/RecordingDashboardEventBroadcaster.cs
+++ b/src/ZB.MOM.WW.MxGateway.Tests/TestSupport/RecordingDashboardEventBroadcaster.cs
@@ -8,7 +8,7 @@ namespace ZB.MOM.WW.MxGateway.Tests.TestSupport;
/// test double that captures every
/// Publish(sessionId, mxEvent) call into a thread-safe list, so tests
/// can prove the gRPC producer loop actually mirrors events to the dashboard
-/// fan-out seam. Tests-026.
+/// fan-out seam.
///
public sealed class RecordingDashboardEventBroadcaster : IDashboardEventBroadcaster
{
diff --git a/src/ZB.MOM.WW.MxGateway.Worker.Tests/Bootstrap/WorkerApplicationTests.cs b/src/ZB.MOM.WW.MxGateway.Worker.Tests/Bootstrap/WorkerApplicationTests.cs
index e1155c3..5b28223 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker.Tests/Bootstrap/WorkerApplicationTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker.Tests/Bootstrap/WorkerApplicationTests.cs
@@ -143,10 +143,7 @@ public sealed class WorkerApplicationTests
private sealed class SucceedingPipeClient : IWorkerPipeClient
{
- /// Runs the worker pipe client successfully.
- /// Worker options.
- /// Cancellation token.
- /// Completed task.
+ ///
public Task RunAsync(
WorkerOptions options,
CancellationToken cancellationToken = default)
@@ -166,10 +163,7 @@ public sealed class WorkerApplicationTests
_exception = exception;
}
- /// Runs the worker pipe client and throws configured exception.
- /// Worker options.
- /// Cancellation token.
- /// Never completes; always throws.
+ ///
public Task RunAsync(
WorkerOptions options,
CancellationToken cancellationToken = default)
diff --git a/src/ZB.MOM.WW.MxGateway.Worker.Tests/Conversion/VariantConverterTests.cs b/src/ZB.MOM.WW.MxGateway.Worker.Tests/Conversion/VariantConverterTests.cs
index 559d7bf..1378bd9 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker.Tests/Conversion/VariantConverterTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker.Tests/Conversion/VariantConverterTests.cs
@@ -132,7 +132,7 @@ public sealed class VariantConverterTests
}
///
- /// Worker-010 regression: a 32-bit with an expected
+ /// A 32-bit with an expected
/// data type of must not be projected as a
/// Windows FILETIME. A uint can only hold the low 32 bits of a FILETIME,
/// which would silently render as a near-1601 timestamp; the converter
diff --git a/src/ZB.MOM.WW.MxGateway.Worker.Tests/Ipc/WorkerFrameProtocolTests.cs b/src/ZB.MOM.WW.MxGateway.Worker.Tests/Ipc/WorkerFrameProtocolTests.cs
index 4b60181..468a81b 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker.Tests/Ipc/WorkerFrameProtocolTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker.Tests/Ipc/WorkerFrameProtocolTests.cs
@@ -14,6 +14,7 @@ public sealed class WorkerFrameProtocolTests
private const string Nonce = "nonce-secret";
/// Verifies that valid envelopes round-trip through write and read.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task WriteAndReadAsync_WithValidEnvelope_RoundTripsFrame()
{
@@ -32,6 +33,7 @@ public sealed class WorkerFrameProtocolTests
}
/// Verifies that wrong protocol version throws mismatch error.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task ReadAsync_WithWrongProtocolVersion_ThrowsProtocolVersionMismatch()
{
@@ -49,6 +51,7 @@ public sealed class WorkerFrameProtocolTests
}
/// Verifies that wrong session ID throws mismatch error.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task ReadAsync_WithWrongSessionId_ThrowsSessionMismatch()
{
@@ -72,6 +75,7 @@ public sealed class WorkerFrameProtocolTests
/// length prefix is the leading four bytes of the stream, so a four-zero-byte
/// stream is exactly a frame declaring a zero-length payload.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task ReadAsync_WithZeroLengthPayload_ThrowsMalformedLength()
{
@@ -93,6 +97,7 @@ public sealed class WorkerFrameProtocolTests
/// A small maximum is configured so the rejection is asserted without
/// allocating a multi-megabyte buffer.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task ReadAsync_WithPayloadAboveConfiguredMaximum_ThrowsMessageTooLarge()
{
@@ -115,6 +120,7 @@ public sealed class WorkerFrameProtocolTests
}
/// Verifies that malformed payload throws invalid envelope error.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task ReadAsync_WithMalformedPayload_ThrowsInvalidEnvelope()
{
@@ -130,7 +136,7 @@ public sealed class WorkerFrameProtocolTests
}
///
- /// Worker.Tests-021 (a): pins the EndOfStream branch of
+ /// Pins the EndOfStream branch of
/// WorkerFrameReader.ReadExactlyOrThrowAsync. The gateway
/// closing its end of the pipe during a partial-frame read is the
/// most common production transport failure; the reader must
@@ -140,6 +146,7 @@ public sealed class WorkerFrameProtocolTests
/// payload but only supplies 50 bytes, so the inner read loop sees
/// bytesRead == 0 mid-frame.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task ReadAsync_WhenStreamEndsMidFrame_ThrowsEndOfStream()
{
@@ -157,16 +164,16 @@ public sealed class WorkerFrameProtocolTests
}
///
- /// Worker.Tests-021 (b): pins the writer-side
- /// MessageTooLarge branch. A session that constructs an
- /// envelope whose serialised size exceeds MaxMessageBytes
- /// must be rejected by the writer before any bytes are sent down
- /// the pipe, so a misbehaving producer cannot push the receiver
- /// past its bounds. A small MaxMessageBytes is configured
- /// so a modest GatewayHello payload — with its nonce
- /// padded out to several hundred bytes — exceeds the limit
+ /// Pins the writer-side MessageTooLarge branch. A session that
+ /// constructs an envelope whose serialised size exceeds
+ /// MaxMessageBytes must be rejected by the writer before any
+ /// bytes are sent down the pipe, so a misbehaving producer cannot
+ /// push the receiver past its bounds. A small MaxMessageBytes
+ /// is configured so a modest GatewayHello payload — with its
+ /// nonce padded out to several hundred bytes — exceeds the limit
/// without allocating anything large.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task WriteAsync_WithEnvelopeAboveConfiguredMaximum_ThrowsMessageTooLarge()
{
@@ -191,11 +198,10 @@ public sealed class WorkerFrameProtocolTests
}
///
- /// Worker.Tests-021 (c): documents that the writer-side
- /// InvalidEnvelope branch (raised when
- /// WorkerEnvelope.CalculateSize() returns 0) is unreachable
- /// through public API. WorkerEnvelopeValidator.Validate (run
- /// before the size check in WorkerFrameWriter.WriteAsync)
+ /// Documents that the writer-side InvalidEnvelope branch
+ /// (raised when WorkerEnvelope.CalculateSize() returns 0) is
+ /// unreachable through public API. WorkerEnvelopeValidator.Validate
+ /// (run before the size check in WorkerFrameWriter.WriteAsync)
/// rejects any envelope whose BodyCase is None with
/// InvalidEnvelope; a body-less envelope is therefore
/// intercepted before the empty-payload branch can fire. Any
@@ -209,6 +215,7 @@ public sealed class WorkerFrameProtocolTests
/// would weaken the writer against future serialisation
/// regressions; this test makes its rationale visible.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task WriteAsync_WithEmptyEnvelope_ThrowsInvalidEnvelopeFromValidator()
{
@@ -233,6 +240,7 @@ public sealed class WorkerFrameProtocolTests
}
/// Verifies that concurrent writes produce complete serialized frames.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task WriteAsync_WithConcurrentCalls_SerializesCompleteFrames()
{
@@ -256,12 +264,13 @@ public sealed class WorkerFrameProtocolTests
}
///
- /// Worker-009 regression: the reader rents its payload buffer from a
- /// shared pool, so a rented buffer can be larger than the current frame
- /// and may carry bytes from a previous, larger frame. Reading frames of
- /// differing sizes back-to-back through one reader must parse each frame
- /// using only its own payload length, never trailing pooled bytes.
+ /// The reader rents its payload buffer from a shared pool, so a rented
+ /// buffer can be larger than the current frame and may carry bytes from
+ /// a previous, larger frame. Reading frames of differing sizes
+ /// back-to-back through one reader must parse each frame using only its
+ /// own payload length, never trailing pooled bytes.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task ReadAsync_WithVaryingFrameSizes_ParsesEachFrameExactly()
{
diff --git a/src/ZB.MOM.WW.MxGateway.Worker.Tests/Ipc/WorkerPipeClientTests.cs b/src/ZB.MOM.WW.MxGateway.Worker.Tests/Ipc/WorkerPipeClientTests.cs
index 5c6dc6f..e120e23 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker.Tests/Ipc/WorkerPipeClientTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker.Tests/Ipc/WorkerPipeClientTests.cs
@@ -15,6 +15,7 @@ namespace ZB.MOM.WW.MxGateway.Worker.Tests.Ipc;
public sealed class WorkerPipeClientTests
{
/// Verifies that worker client connects and completes handshake.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task RunAsync_ConnectsToPipeAndCompletesHandshake()
{
@@ -83,6 +84,7 @@ public sealed class WorkerPipeClientTests
}
/// Verifies that worker client retries until pipe server becomes available.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task RunAsync_RetriesUntilPipeServerAppears()
{
@@ -127,6 +129,7 @@ public sealed class WorkerPipeClientTests
}
/// Verifies that worker client throws timeout if pipe never appears.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task RunAsync_WhenPipeNeverAppears_ThrowsTimeoutException()
{
@@ -148,7 +151,7 @@ public sealed class WorkerPipeClientTests
///
/// Reads frames until one matching the expected body case is found,
/// skipping interleaved heartbeats (the first heartbeat is emitted
- /// immediately on entering the heartbeat loop — see Worker-002).
+ /// immediately on entering the heartbeat loop).
///
private static async Task ReadUntilAsync(
WorkerFrameReader reader,
diff --git a/src/ZB.MOM.WW.MxGateway.Worker.Tests/Ipc/WorkerPipeSessionTests.cs b/src/ZB.MOM.WW.MxGateway.Worker.Tests/Ipc/WorkerPipeSessionTests.cs
index b269b7d..0c13b3a 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker.Tests/Ipc/WorkerPipeSessionTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker.Tests/Ipc/WorkerPipeSessionTests.cs
@@ -20,6 +20,7 @@ public sealed class WorkerPipeSessionTests
private const string Nonce = "nonce-secret";
/// Verifies that valid gateway hello triggers worker hello and ready responses.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task CompleteStartupHandshakeAsync_WithValidGatewayHello_SendsHelloThenReady()
{
@@ -51,6 +52,7 @@ public sealed class WorkerPipeSessionTests
}
/// Verifies that wrong nonce causes protocol violation fault before initialization.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task CompleteStartupHandshakeAsync_WithWrongNonce_FaultsBeforeInitialization()
{
@@ -79,6 +81,7 @@ public sealed class WorkerPipeSessionTests
}
/// Verifies that unsupported protocol version causes mismatch fault before initialization.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task CompleteStartupHandshakeAsync_WithWrongProtocol_FaultsBeforeInitialization()
{
@@ -106,6 +109,7 @@ public sealed class WorkerPipeSessionTests
}
/// Verifies that malformed frame causes protocol violation fault.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task CompleteStartupHandshakeAsync_WithMalformedFrame_WritesWorkerFault()
{
@@ -132,6 +136,7 @@ public sealed class WorkerPipeSessionTests
}
/// Verifies that MXAccess COM creation failure produces fault instead of ready.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task CompleteStartupHandshakeAsync_WhenMxAccessCreationFails_WritesFaultInsteadOfReady()
{
@@ -158,6 +163,7 @@ public sealed class WorkerPipeSessionTests
}
/// Verifies that heartbeat payload reflects current runtime snapshot.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task RunAsync_SendsHeartbeatPayloadFromRuntimeSnapshot()
{
@@ -211,6 +217,7 @@ public sealed class WorkerPipeSessionTests
}
/// Verifies that heartbeat reports current command correlation during execution.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task RunAsync_WhenCommandIsExecuting_HeartbeatReportsCurrentCorrelation()
{
@@ -258,6 +265,7 @@ public sealed class WorkerPipeSessionTests
}
/// Verifies that worker events are written to the pipe.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task RunAsync_WhenRuntimeHasEvents_WritesWorkerEventEnvelope()
{
@@ -293,6 +301,7 @@ public sealed class WorkerPipeSessionTests
/// (not dispatched to the STA) with an OK reply that echoes the ping
/// message into the reply's diagnostic field.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task RunAsync_PingControlCommand_RepliesOkAndEchoesMessage()
{
@@ -324,6 +333,7 @@ public sealed class WorkerPipeSessionTests
/// Verifies that GetSessionState reports the worker's lifecycle as the
/// proto SessionState — READY while the message loop is serving.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task RunAsync_GetSessionStateControlCommand_RepliesReady()
{
@@ -359,6 +369,7 @@ public sealed class WorkerPipeSessionTests
/// Verifies that GetWorkerInfo populates the worker process id, version,
/// and MXAccess ProgID/CLSID from the worker's own metadata.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task RunAsync_GetWorkerInfoControlCommand_PopulatesWorkerInfoFields()
{
@@ -398,6 +409,7 @@ public sealed class WorkerPipeSessionTests
/// Verifies that DrainEvents drains the runtime session's queued events
/// into the reply rather than streaming them as WorkerEvent envelopes.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task RunAsync_DrainEventsControlCommand_ReturnsQueuedEvents()
{
@@ -442,6 +454,7 @@ public sealed class WorkerPipeSessionTests
/// shutdown runs and disposes the runtime session, and that the message
/// loop then stops.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task RunAsync_ShutdownWorkerControlCommand_RepliesOkThenShutsDown()
{
@@ -491,12 +504,12 @@ public sealed class WorkerPipeSessionTests
///
/// Verifies that stale STA activity with no command in flight triggers
- /// the watchdog StaHung fault. Worker-017 changed the watchdog to skip
- /// the fault while a command is in flight (the worker is busy
- /// executing it, not hung), so this test deliberately leaves the
- /// current-command correlation id empty to assert the genuine-hung
- /// path still fires.
+ /// the watchdog StaHung fault. The watchdog skips the fault while a
+ /// command is in flight (the worker is busy executing it, not hung),
+ /// so this test deliberately leaves the current-command correlation
+ /// id empty to assert the genuine-hung path still fires.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task RunAsync_WhenStaActivityIsStale_WritesWatchdogFault()
{
@@ -532,17 +545,18 @@ public sealed class WorkerPipeSessionTests
}
///
- /// Worker-017 regression: while a command is in flight (snapshot's
- /// current command correlation id is non-empty), stale STA activity
- /// must NOT trigger the watchdog StaHung fault. The STA is busy
- /// executing the command, not hung; StaRuntime.ProcessQueuedCommands
- /// only calls MarkActivity() before and after each work item,
- /// so a synchronously long-running command (e.g. ReadBulk
- /// waiting timeout_ms for OnDataChange) legitimately freezes
+ /// While a command is in flight (snapshot's current command
+ /// correlation id is non-empty), stale STA activity must NOT trigger
+ /// the watchdog StaHung fault. The STA is busy executing the command,
+ /// not hung; StaRuntime.ProcessQueuedCommands only calls
+ /// MarkActivity() before and after each work item, so a
+ /// synchronously long-running command (e.g. ReadBulk waiting
+ /// timeout_ms for OnDataChange) legitimately freezes
/// LastActivityUtc. The heartbeat already advertises the
/// in-flight correlation id so the gateway can apply its own per-command
/// timeout.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task RunAsync_WhenStaActivityIsStaleWithCommandInFlight_DoesNotWriteWatchdogFault()
{
@@ -596,13 +610,13 @@ public sealed class WorkerPipeSessionTests
}
///
- /// Worker-004 regression: once the watchdog reports an StaHung fault,
- /// subsequent heartbeats must report
- /// rather than a non-faulted state that contradicts the fault. The
- /// snapshot uses an empty current-command correlation id so the
- /// heartbeat State is derived from the session state, not forced to
- /// ExecutingCommand.
+ /// Once the watchdog reports an StaHung fault, subsequent heartbeats
+ /// must report rather than a
+ /// non-faulted state that contradicts the fault. The snapshot uses an
+ /// empty current-command correlation id so the heartbeat State is
+ /// derived from the session state, not forced to ExecutingCommand.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task RunAsync_AfterWatchdogFault_HeartbeatReportsFaultedState()
{
@@ -643,15 +657,16 @@ public sealed class WorkerPipeSessionTests
}
///
- /// Worker-023 regression: the in-flight-command suppression on the
- /// StaHung watchdog (Worker-017) is bounded by
- /// WorkerPipeSessionOptions.HeartbeatStuckCeiling. A truly
- /// stuck synchronous STA command (e.g. a dead MXAccess provider) would
- /// otherwise keep CurrentCommandCorrelationId non-empty forever
- /// and permanently defeat the watchdog. Once LastStaActivityUtc
- /// has been stale for longer than HeartbeatStuckCeiling the
- /// watchdog DOES fire StaHung even with a command in flight.
+ /// The in-flight-command suppression on the StaHung watchdog is
+ /// bounded by WorkerPipeSessionOptions.HeartbeatStuckCeiling. A
+ /// truly stuck synchronous STA command (e.g. a dead MXAccess provider)
+ /// would otherwise keep CurrentCommandCorrelationId non-empty
+ /// forever and permanently defeat the watchdog. Once
+ /// LastStaActivityUtc has been stale for longer than
+ /// HeartbeatStuckCeiling the watchdog DOES fire StaHung
+ /// even with a command in flight.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task RunAsync_WhenStaActivityIsStaleBeyondCeilingWithCommandInFlight_WritesWatchdogFault()
{
@@ -690,10 +705,11 @@ public sealed class WorkerPipeSessionTests
}
///
- /// Worker-025 regression: RunAsync must throw a diagnostic
- /// exception if the runtime-session factory returns null, rather than
- /// deferring the failure to an NRE on the next dereference.
+ /// RunAsync must throw a diagnostic exception if the
+ /// runtime-session factory returns null, rather than deferring the
+ /// failure to an NRE on the next dereference.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task RunAsync_WhenRuntimeSessionFactoryReturnsNull_ThrowsDiagnosticException()
{
@@ -715,11 +731,11 @@ public sealed class WorkerPipeSessionTests
}
///
- /// Worker-006 regression: when graceful shutdown times out, RunAsync
- /// must still dispose the runtime session in its finally block.
- /// Skipping disposal on the timed-out path leaked the STA thread and
- /// the MXAccess COM object.
+ /// When graceful shutdown times out, RunAsync must still dispose the
+ /// runtime session in its finally block. Skipping disposal on the
+ /// timed-out path leaked the STA thread and the MXAccess COM object.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task RunAsync_WhenShutdownTimesOut_StillDisposesRuntimeSession()
{
@@ -783,6 +799,7 @@ public sealed class WorkerPipeSessionTests
}
/// Verifies that shutdown drops late replies and sends shutdown ack.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task RunAsync_WhenShutdownArrivesDuringCommand_DropsLateReplyAndWritesShutdownAck()
{
@@ -823,6 +840,7 @@ public sealed class WorkerPipeSessionTests
}
/// Verifies that command exceptions after shutdown are dropped before ack.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task RunAsync_WhenCommandThrowsAfterShutdown_DropsLateFaultAndWritesShutdownAck()
{
@@ -852,9 +870,9 @@ public sealed class WorkerPipeSessionTests
await pipePair.GatewayWriter
.WriteAsync(CreateShutdownEnvelope(), cancellation.Token);
- // The first heartbeat is emitted immediately on entering the loop
- // (Worker-002), so skip any interleaved heartbeats; the late fault
- // must still be dropped — no WorkerFault may precede the ack.
+ // The first heartbeat is emitted immediately on entering the loop,
+ // so skip any interleaved heartbeats; the late fault must still be
+ // dropped — no WorkerFault may precede the ack.
WorkerEnvelope envelopeAfterShutdown;
do
{
@@ -873,7 +891,7 @@ public sealed class WorkerPipeSessionTests
}
///
- /// Worker.Tests-017 regression: the WorkerCancel branch of
+ /// The WorkerCancel branch of
/// must
/// forward the envelope's correlation id to the runtime session via
/// and keep the
@@ -881,6 +899,7 @@ public sealed class WorkerPipeSessionTests
/// returns true (keep reading), so a subsequent
/// WorkerShutdown still produces the normal shutdown ack.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task RunAsync_WhenGatewaySendsWorkerCancel_ForwardsCorrelationIdToRuntimeSession()
{
@@ -910,7 +929,7 @@ public sealed class WorkerPipeSessionTests
}
///
- /// Worker.Tests-017 regression: the default: arm of
+ /// The default: arm of
/// must
/// throw with
///
@@ -923,6 +942,7 @@ public sealed class WorkerPipeSessionTests
/// pre-handshake protocol violations); the contract this test pins
/// is the exception type/error-code and message-loop exit.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task RunAsync_WhenGatewaySendsUnexpectedEnvelopeBodyAfterHandshake_ThrowsAndExitsMessageLoop()
{
@@ -956,11 +976,12 @@ public sealed class WorkerPipeSessionTests
}
///
- /// Worker-002 regression: the first heartbeat must be emitted
- /// immediately on entering the heartbeat loop, not after a full
- /// HeartbeatInterval. A long interval is configured so a delay-first
- /// loop would fail to deliver a heartbeat inside the assertion window.
+ /// The first heartbeat must be emitted immediately on entering the
+ /// heartbeat loop, not after a full HeartbeatInterval. A long interval
+ /// is configured so a delay-first loop would fail to deliver a
+ /// heartbeat inside the assertion window.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task RunAsync_SendsFirstHeartbeatImmediatelyOnEnteringLoop()
{
@@ -984,7 +1005,7 @@ public sealed class WorkerPipeSessionTests
// if the first heartbeat is not received within 5s, ReadUntilAsync throws
// OperationCanceledException and the test fails. A redundant wall-clock
// elapsed < 5s assertion would add the same class of flakiness
- // Workers.Tests-003/004/013/020 corrected elsewhere, so it is omitted here.
+ // corrected elsewhere, so it is omitted here.
using CancellationTokenSource heartbeatWait = CancellationTokenSource
.CreateLinkedTokenSource(cancellation.Token);
heartbeatWait.CancelAfter(TimeSpan.FromSeconds(5));
@@ -999,11 +1020,12 @@ public sealed class WorkerPipeSessionTests
}
///
- /// Worker-003 regression: when a command completes after the worker
- /// has transitioned out of a command-serving state, the dropped
- /// reply must be logged with a diagnostic rather than discarded
- /// silently, so a stuck gateway correlation wait can be traced.
+ /// When a command completes after the worker has transitioned out of
+ /// a command-serving state, the dropped reply must be logged with a
+ /// diagnostic rather than discarded silently, so a stuck gateway
+ /// correlation wait can be traced.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task RunAsync_WhenReplyIsDroppedAfterShutdown_LogsDiagnostic()
{
@@ -1110,7 +1132,7 @@ public sealed class WorkerPipeSessionTests
// (default = position in the typical Hello/Command/Cancel/Shutdown
// ordering) so a multi-frame test that interleaves the helpers can
// assign monotonically increasing values and produce a wire trace
- // that reads in ascending order — see Worker.Tests-030.
+ // that reads in ascending order.
private static WorkerEnvelope CreateGatewayHelloEnvelope(
string nonce = Nonce,
uint supportedProtocolVersion = GatewayContractInfo.WorkerProtocolVersion,
@@ -1346,17 +1368,13 @@ public sealed class WorkerPipeSessionTests
}
}
- /// Records an informational log event.
- /// The event name.
- /// The event fields.
+ ///
public void Information(string eventName, IReadOnlyDictionary fields)
{
Record(eventName, fields);
}
- /// Records an error log event.
- /// The event name.
- /// The event fields.
+ ///
public void Error(string eventName, IReadOnlyDictionary fields)
{
Record(eventName, fields);
@@ -1447,7 +1465,7 @@ public sealed class WorkerPipeSessionTests
return new PipePair(gatewayStream, workerStream);
}
- /// Disposes pipe resources.
+ ///
public void Dispose()
{
WorkerStream.Dispose();
diff --git a/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/AlarmCommandExecutorTests.cs b/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/AlarmCommandExecutorTests.cs
index 9c6e55b..bef898a 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/AlarmCommandExecutorTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/AlarmCommandExecutorTests.cs
@@ -371,28 +371,20 @@ public sealed class AlarmCommandExecutorTests
/// Gets the last alarm filter prefix.
public string? LastFilterPrefix { get; private set; }
- /// Records a subscription.
- /// The subscribe-alarms command.
- /// The session identifier.
+ ///
public void Subscribe(SubscribeAlarmsCommand command, string sessionId)
{
LastSubscription = command.SubscriptionExpression;
LastSessionId = sessionId;
}
- /// Records an unsubscribe request.
+ ///
public void Unsubscribe()
{
UnsubscribeCalled = true;
}
- /// Records an acknowledge request.
- /// The alarm identifier.
- /// The acknowledge comment.
- /// The operator user name.
- /// The operator node name.
- /// The operator domain.
- /// The operator full name.
+ ///
public int Acknowledge(
Guid alarmGuid, string comment, string operatorUser,
string operatorNode, string operatorDomain, string operatorFullName)
@@ -406,15 +398,7 @@ public sealed class AlarmCommandExecutorTests
return AcknowledgeReturn;
}
- /// Records an acknowledge by name request.
- /// The alarm name.
- /// The provider name.
- /// The group name.
- /// The acknowledge comment.
- /// The operator user name.
- /// The operator node name.
- /// The operator domain.
- /// The operator full name.
+ ///
public int AcknowledgeByName(
string alarmName, string providerName, string groupName,
string comment, string operatorUser, string operatorNode,
@@ -428,8 +412,7 @@ public sealed class AlarmCommandExecutorTests
/// Gets the last acknowledge by name tuple.
public (string Name, string Provider, string Group)? LastAckByNameTuple { get; private set; }
- /// Queries the active alarms with the given filter prefix.
- /// The alarm filter prefix for the query.
+ ///
public IReadOnlyList QueryActive(string? alarmFilterPrefix)
{
LastFilterPrefix = alarmFilterPrefix;
@@ -439,7 +422,7 @@ public sealed class AlarmCommandExecutorTests
/// Gets the number of poll calls.
public int PollCount { get; private set; }
- /// Increments the poll count.
+ ///
public void PollOnce()
{
PollCount++;
diff --git a/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/AlarmCommandHandlerTests.cs b/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/AlarmCommandHandlerTests.cs
index 87382fe..5e95c0e 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/AlarmCommandHandlerTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/AlarmCommandHandlerTests.cs
@@ -42,7 +42,7 @@ public sealed class AlarmCommandHandlerTests
}
///
- /// Worker.Tests-024: pins both the disposal contract and the
+ /// Pins both the disposal contract and the
/// origin of the propagated exception. The fake throws
/// InvalidOperationException("simulated wnwrap subscribe failure")
/// from Subscribe; the handler must propagate that exact
@@ -199,7 +199,7 @@ public sealed class AlarmCommandHandlerTests
}
///
- /// Worker-024 regression: every method that touches the underlying
+ /// Every method that touches the underlying
/// must invoke the configured
/// STA-affinity guard. A guard that throws (simulating an off-STA
/// call) must propagate from every command-path entry point.
@@ -238,7 +238,7 @@ public sealed class AlarmCommandHandlerTests
}
///
- /// Worker-024 regression: a guard that throws must propagate from
+ /// A guard that throws must propagate from
/// every command-path entry point — proving the guard is not
/// swallowed by an inner try/catch.
///
@@ -274,7 +274,7 @@ public sealed class AlarmCommandHandlerTests
}
///
- /// Worker-9: ForcedMode=Subtag builds a subtag consumer (via the
+ /// ForcedMode=Subtag builds a subtag consumer (via the
/// injected standby factory) and advises it — the primary
/// (alarmmgr) consumer is NOT created.
///
@@ -312,7 +312,7 @@ public sealed class AlarmCommandHandlerTests
}
///
- /// Worker-9: ForcedMode=Unspecified + a non-empty watch list builds a
+ /// ForcedMode=Unspecified + a non-empty watch list builds a
/// failover composite (primary + subtag standby). Forcing the primary
/// to fail on subscribe with a threshold of 1 drives the composite to
/// switch to the subtag provider, which must enqueue an
@@ -359,7 +359,7 @@ public sealed class AlarmCommandHandlerTests
}
///
- /// Worker-9: a non-failover subscribe (alarmmgr-only) never enqueues a
+ /// A non-failover subscribe (alarmmgr-only) never enqueues a
/// provider-mode-changed event, and a subsequent Unsubscribe detaches
/// the handler so no event leaks.
///
@@ -378,7 +378,7 @@ public sealed class AlarmCommandHandlerTests
}
///
- /// Worker-9: the mapper builds a well-formed OnAlarmProviderModeChanged
+ /// The mapper builds a well-formed OnAlarmProviderModeChanged
/// MxEvent — correct family and populated body fields.
///
[Fact]
@@ -436,8 +436,7 @@ public sealed class AlarmCommandHandlerTests
/// Gets a value indicating whether the consumer has been disposed.
public bool Disposed { get; private set; }
- /// Subscribes to alarms with the given subscription string.
- /// The subscription reference.
+ ///
public void Subscribe(string subscription)
{
LastSubscription = subscription;
@@ -447,13 +446,7 @@ public sealed class AlarmCommandHandlerTests
}
}
- /// Acknowledges an alarm by GUID.
- /// The alarm GUID.
- /// The acknowledgment comment.
- /// The operator name.
- /// The operator node.
- /// The operator domain.
- /// The operator full name.
+ ///
public int AcknowledgeByGuid(
Guid alarmGuid, string ackComment, string ackOperatorName,
string ackOperatorNode, string ackOperatorDomain, string ackOperatorFullName)
@@ -463,15 +456,7 @@ public sealed class AlarmCommandHandlerTests
return AcknowledgeReturn;
}
- /// Acknowledges an alarm by name.
- /// The alarm name.
- /// The provider name.
- /// The alarm group name.
- /// The acknowledgment comment.
- /// The operator name.
- /// The operator node.
- /// The operator domain.
- /// The operator full name.
+ ///
public int AcknowledgeByName(
string alarmName, string providerName, string groupName,
string ackComment, string ackOperatorName, string ackOperatorNode,
@@ -485,13 +470,13 @@ public sealed class AlarmCommandHandlerTests
/// Gets the last acknowledge-by-name parameters.
public (string Name, string Provider, string Group)? LastAckByNameTuple { get; private set; }
- /// Returns a snapshot of active alarms.
+ ///
public IReadOnlyList SnapshotActiveAlarms() => SnapshotResult;
/// Gets the number of times polled.
public int PollCount { get; private set; }
- /// Polls once for alarm updates.
+ ///
public void PollOnce()
{
PollCount++;
diff --git a/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/AlarmDispatcherTests.cs b/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/AlarmDispatcherTests.cs
index c30ac6f..4106c79 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/AlarmDispatcherTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/AlarmDispatcherTests.cs
@@ -394,20 +394,13 @@ public sealed class AlarmDispatcherTests
AlarmTransitionEmitted?.Invoke(this, transition);
}
- /// Records the subscription reference.
- /// The subscription reference.
+ ///
public void Subscribe(string subscription)
{
LastSubscription = subscription;
}
- /// Records an acknowledge-by-GUID call with operator identity.
- /// The alarm GUID.
- /// The acknowledgment comment.
- /// The operator name.
- /// The operator node.
- /// The operator domain.
- /// The operator full name.
+ ///
public int AcknowledgeByGuid(
Guid alarmGuid,
string ackComment,
@@ -425,15 +418,7 @@ public sealed class AlarmDispatcherTests
return AcknowledgeReturn;
}
- /// Records an acknowledge-by-name call with alarm name, provider, and group.
- /// The alarm name.
- /// The provider name.
- /// The alarm group name.
- /// The acknowledgment comment.
- /// The operator name.
- /// The operator node.
- /// The operator domain.
- /// The operator full name.
+ ///
public int AcknowledgeByName(
string alarmName, string providerName, string groupName,
string ackComment, string ackOperatorName, string ackOperatorNode,
@@ -447,7 +432,7 @@ public sealed class AlarmDispatcherTests
/// Gets the last acknowledge-by-name tuple (alarm name, provider, group).
public (string Name, string Provider, string Group)? LastAckByNameTuple { get; private set; }
- /// Returns the current snapshot result collection.
+ ///
public IReadOnlyList SnapshotActiveAlarms()
{
return SnapshotResult;
@@ -456,7 +441,7 @@ public sealed class AlarmDispatcherTests
/// Gets the count of poll operations.
public int PollCount { get; private set; }
- /// Increments the poll count.
+ ///
public void PollOnce()
{
PollCount++;
diff --git a/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/FailoverAlarmConsumerTests.cs b/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/FailoverAlarmConsumerTests.cs
index 575bcfa..71f5f8d 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/FailoverAlarmConsumerTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/FailoverAlarmConsumerTests.cs
@@ -24,6 +24,7 @@ public sealed class FailoverAlarmConsumerTests
///
private sealed class FlakyPrimary : IMxAccessAlarmConsumer
{
+ /// Raised when the fake forwards a simulated alarm transition via .
public event EventHandler? AlarmTransitionEmitted;
public bool ThrowOnPoll = true;
@@ -32,7 +33,7 @@ public sealed class FailoverAlarmConsumerTests
/// When set, throws
/// instead of a
/// , to
- /// exercise the OOM-safe exception filter (Worker.Tests-032).
+ /// exercise the OOM-safe exception filter.
///
public bool ThrowOutOfMemoryOnPoll;
@@ -45,6 +46,7 @@ public sealed class FailoverAlarmConsumerTests
///
public int SubscribeCount;
+ ///
public void Subscribe(string s)
{
SubscribeCount++;
@@ -54,6 +56,7 @@ public sealed class FailoverAlarmConsumerTests
}
}
+ ///
public void PollOnce()
{
Polls++;
@@ -68,14 +71,20 @@ public sealed class FailoverAlarmConsumerTests
}
}
+ ///
public int AcknowledgeByGuid(Guid g, string c, string a, string b, string d, string e) => 11;
+ ///
public int AcknowledgeByName(string n, string p, string gr, string c, string a, string b, string d, string e) => 11;
+ ///
public IReadOnlyList SnapshotActiveAlarms() => Array.Empty();
+ ///
public void Dispose() { }
+ /// Raises with the given event, simulating a COM-forwarded transition.
+ /// The transition event to forward.
public void Raise(MxAlarmTransitionEvent e) => AlarmTransitionEmitted?.Invoke(this, e);
}
@@ -85,27 +94,33 @@ public sealed class FailoverAlarmConsumerTests
///
private sealed class StubStandby : IMxAccessAlarmConsumer
{
+ /// Raised when the fake forwards a simulated alarm transition via .
public event EventHandler? AlarmTransitionEmitted;
public bool Subscribed;
///
/// When set, throws — modeling a
- /// priming-snapshot failure during failover (Worker-026).
+ /// priming-snapshot failure during failover.
///
public bool ThrowOnSnapshot;
/// Number of calls.
public int SnapshotCalls;
+ ///
public void Subscribe(string s) => Subscribed = true;
+ ///
public void PollOnce() { }
+ ///
public int AcknowledgeByGuid(Guid g, string c, string a, string b, string d, string e) => 22;
+ ///
public int AcknowledgeByName(string n, string p, string gr, string c, string a, string b, string d, string e) => 22;
+ ///
public IReadOnlyList SnapshotActiveAlarms()
{
SnapshotCalls++;
@@ -117,8 +132,11 @@ public sealed class FailoverAlarmConsumerTests
return Array.Empty();
}
+ ///
public void Dispose() { }
+ /// Raises with the given event, simulating a COM-forwarded transition.
+ /// The transition event to forward.
public void Raise(MxAlarmTransitionEvent e) => AlarmTransitionEmitted?.Invoke(this, e);
}
@@ -128,6 +146,7 @@ public sealed class FailoverAlarmConsumerTests
PreviousState = MxAlarmStateKind.Unspecified,
};
+ /// Proves that the consumer switches to the subtag standby after the primary fails the configured threshold of consecutive times.
[Fact]
public void Primary_FailsThresholdTimes_SwitchesToSubtag()
{
@@ -154,6 +173,7 @@ public sealed class FailoverAlarmConsumerTests
Assert.Equal(unchecked((int)0x80004005), changes[0].HResult);
}
+ /// Proves that once failed over, transitions raised by the standby are forwarded through the consumer's AlarmTransitionEmitted event.
[Fact]
public void AfterSwitch_StandbyTransitionsAreForwarded()
{
@@ -174,6 +194,7 @@ public sealed class FailoverAlarmConsumerTests
Assert.Same(transition, forwarded);
}
+ /// Proves that once the primary heals, the consumer fails back to it only after the configured number of consecutive clean probes.
[Fact]
public void WhileDegraded_PrimaryHeals_FailsBackAfterStableProbes()
{
@@ -212,6 +233,7 @@ public sealed class FailoverAlarmConsumerTests
Assert.Equal(subscribeCountAfterFailover, primary.SubscribeCount);
}
+ /// Proves that before any failover, transitions raised by the primary are forwarded but transitions from the inactive standby are suppressed.
[Fact]
public void BeforeFailover_PrimaryTransitionsAreForwarded()
{
@@ -252,7 +274,7 @@ public sealed class FailoverAlarmConsumerTests
FailoverSettings settings = new FailoverSettings(threshold: 1, probeIntervalSeconds: 0, stableProbes: 3);
using FailoverAlarmConsumer sut = new FailoverAlarmConsumer(primary, standby, settings);
- sut.Subscribe(@"\\HOST\Galaxy!Area"); // Subscribe attempt #1 (throws) → Subtag
+ sut.Subscribe(@"\\HOST\Galaxy!Area"); // Subscribe attempt (throws) → Subtag
// Capture how many Subscribe calls the initial setup caused (exactly 1:
// the attempt that threw and triggered failover).
@@ -272,6 +294,7 @@ public sealed class FailoverAlarmConsumerTests
Assert.Equal(subscribeCountAfterSetup, primary.SubscribeCount);
}
+ /// Proves that acknowledgment calls delegate to whichever child (primary or standby) is currently active.
[Fact]
public void Acknowledge_DelegatesToActiveChild()
{
@@ -325,7 +348,7 @@ public sealed class FailoverAlarmConsumerTests
}
///
- /// Worker-026 regression: when the standby's priming
+ /// When the standby's priming
/// SnapshotActiveAlarms throws during failover, the switch must
/// still (a) fire ProviderModeChanged so the gateway learns the
/// feed went degraded, (b) leave
@@ -361,7 +384,7 @@ public sealed class FailoverAlarmConsumerTests
}
///
- /// Worker-026 regression: when a ProviderModeChanged subscriber's
+ /// When a ProviderModeChanged subscriber's
/// handler throws (modeling the AlarmCommandHandler's event-queue enqueue
/// overflowing at capacity), the switch must still take effect and the
/// exception must not escape the switch path into the poll loop.
@@ -389,7 +412,7 @@ public sealed class FailoverAlarmConsumerTests
}
///
- /// Worker.Tests-031 regression: with a non-zero
+ /// With a non-zero
/// , two back-to-back
/// ProbeOnce calls must throttle — the second falls inside the
/// interval and must NOT re-poll the primary. Two consecutive calls
@@ -422,7 +445,7 @@ public sealed class FailoverAlarmConsumerTests
}
///
- /// Worker.Tests-032 regression: RunPrimary's
+ /// RunPrimary's
/// when (ex is not OutOfMemoryException) filter must let an
/// propagate rather than swallowing it
/// and counting it toward the failover threshold. No mode change must
@@ -447,11 +470,17 @@ public sealed class FailoverAlarmConsumerTests
}
///
- /// Worker.Tests-032 regression: clamps
- /// sub-1 threshold and stableProbes (and sub-0
+ /// Verifies that clamps out-of-range
+ /// threshold and stableProbes values (and negative
/// probeIntervalSeconds) to their safe minimums so a misconfigured
/// bind cannot change failover semantics.
///
+ /// The raw, possibly out-of-range threshold to construct with.
+ /// The raw, possibly negative probe interval (seconds) to construct with.
+ /// The raw, possibly out-of-range stable-probe count to construct with.
+ /// The expected clamped .
+ /// The expected clamped .
+ /// The expected clamped .
[Theory]
[InlineData(0, 0, 0, 1, 0, 1)]
[InlineData(-5, -5, -5, 1, 0, 1)]
diff --git a/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/LmxSubtagAlarmSourceTests.cs b/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/LmxSubtagAlarmSourceTests.cs
index 57c0f81..2b5b7fb 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/LmxSubtagAlarmSourceTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/LmxSubtagAlarmSourceTests.cs
@@ -199,26 +199,39 @@ public sealed class LmxSubtagAlarmSourceTests
private readonly Dictionary handlesByAddress = new(StringComparer.Ordinal);
private int nextItemHandle = 100;
+ /// Item definitions passed to or , in call order.
public List AddedItems { get; } = new();
+ /// Number of times has been called.
public int AdviseCount { get; private set; }
+ /// Server handles passed to , in call order.
public List AdvisedServerHandles { get; } = new();
+ /// Recorded calls, in call order.
public List Writes { get; } = new();
+ /// Item handles passed to , in call order.
public List UnAdvisedItemHandles { get; } = new();
+ /// Item handles passed to , in call order.
public List RemovedItemHandles { get; } = new();
+ /// Number of times has been called.
public int UnregisterCount { get; private set; }
+ /// Returns the item handle most recently assigned to the given item address.
+ /// The item definition/address to look up.
+ /// The item handle assigned by .
public int LastItemHandleFor(string itemAddress) => handlesByAddress[itemAddress];
+ ///
public int Register(string clientName) => FakeServerHandle;
+ ///
public void Unregister(int serverHandle) => UnregisterCount++;
+ ///
public int AddItem(int serverHandle, string itemDefinition)
{
AddedItems.Add(itemDefinition);
@@ -227,55 +240,76 @@ public sealed class LmxSubtagAlarmSourceTests
return handle;
}
+ ///
public int AddItem2(int serverHandle, string itemDefinition, string itemContext)
=> AddItem(serverHandle, itemDefinition);
+ ///
public void RemoveItem(int serverHandle, int itemHandle) => RemovedItemHandles.Add(itemHandle);
+ ///
public void Advise(int serverHandle, int itemHandle)
{
AdviseCount++;
AdvisedServerHandles.Add(serverHandle);
}
+ ///
public void UnAdvise(int serverHandle, int itemHandle) => UnAdvisedItemHandles.Add(itemHandle);
+ ///
public void AdviseSupervisory(int serverHandle, int itemHandle)
{
}
+ ///
public void Write(int serverHandle, int itemHandle, object? value, int userId)
=> Writes.Add(new WriteRecord(serverHandle, itemHandle, value, userId));
+ ///
public void Write2(int serverHandle, int itemHandle, object? value, object? timestamp, int userId)
{
}
+ ///
public void WriteSecured(int serverHandle, int itemHandle, int currentUserId, int verifierUserId, object? value)
{
}
+ ///
public void WriteSecured2(int serverHandle, int itemHandle, int currentUserId, int verifierUserId, object? value, object? timestamp)
{
}
+ ///
public object Suspend(int serverHandle, int itemHandle) => new object();
+ ///
public object Activate(int serverHandle, int itemHandle) => new object();
+ ///
public int AuthenticateUser(int serverHandle, string verifyUser, string verifyUserPassword) => 0;
+ ///
public int ArchestrAUserToId(int serverHandle, string userIdGuid) => 0;
+ ///
public int AddBufferedItem(int serverHandle, string itemDefinition, string itemContext)
=> AddItem(serverHandle, itemDefinition);
+ ///
public void SetBufferedUpdateInterval(int serverHandle, int updateIntervalMilliseconds)
{
}
+ /// Immutable record of a single call captured by .
internal sealed class WriteRecord
{
+ /// Initializes a new instance of the class from a recorded write.
+ /// The server handle the write targeted.
+ /// The item handle the write targeted.
+ /// The written value.
+ /// The writing user's identifier.
public WriteRecord(int serverHandle, int itemHandle, object? value, int userId)
{
ServerHandle = serverHandle;
@@ -284,12 +318,16 @@ public sealed class LmxSubtagAlarmSourceTests
UserId = userId;
}
+ /// The server handle the write targeted.
public int ServerHandle { get; }
+ /// The item handle the write targeted.
public int ItemHandle { get; }
+ /// The written value.
public object? Value { get; }
+ /// The writing user's identifier.
public int UserId { get; }
}
}
diff --git a/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/MxAccessComServerTests.cs b/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/MxAccessComServerTests.cs
index 5a795b0..dd737bb 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/MxAccessComServerTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/MxAccessComServerTests.cs
@@ -5,7 +5,7 @@ using ZB.MOM.WW.MxGateway.Worker.MxAccess;
namespace ZB.MOM.WW.MxGateway.Worker.Tests.MxAccess;
///
-/// Worker-007 regression tests for . The
+/// Regression tests for . The
/// adapter no longer falls back to late-bound Type.InvokeMember
/// reflection: a COM object must implement either the typed
/// ILMXProxyServer COM interface family (production) or
@@ -124,8 +124,7 @@ public sealed class MxAccessComServerTests
/// Gets the recorded method calls as strings.
public IReadOnlyList Calls => calls.ToArray();
- /// Records a Register call and returns the configured handle.
- /// The client name to record.
+ ///
public int Register(string clientName)
{
calls.Add($"Register:{clientName}");
@@ -138,159 +137,111 @@ public sealed class MxAccessComServerTests
return registerHandle;
}
- /// Records an Unregister call.
- /// The MXAccess server handle.
+ ///
public void Unregister(int serverHandle)
{
calls.Add($"Unregister:{serverHandle}");
}
- /// Records an AddItem call and returns zero.
- /// The MXAccess server handle.
- /// The item definition string to record.
+ ///
public int AddItem(int serverHandle, string itemDefinition)
{
calls.Add($"AddItem:{serverHandle}:{itemDefinition}");
return 0;
}
- /// Records an AddItem2 call and returns zero.
- /// The MXAccess server handle.
- /// The item definition string to record.
- /// The item context string to record.
+ ///
public int AddItem2(int serverHandle, string itemDefinition, string itemContext)
{
calls.Add($"AddItem2:{serverHandle}:{itemDefinition}:{itemContext}");
return 0;
}
- /// Records a RemoveItem call.
- /// The MXAccess server handle.
- /// The MXAccess item handle.
+ ///
public void RemoveItem(int serverHandle, int itemHandle)
{
calls.Add($"RemoveItem:{serverHandle}:{itemHandle}");
}
- /// Records an Advise call.
- /// The MXAccess server handle.
- /// The MXAccess item handle.
+ ///
public void Advise(int serverHandle, int itemHandle)
{
calls.Add($"Advise:{serverHandle}:{itemHandle}");
}
- /// Records an UnAdvise call.
- /// The MXAccess server handle.
- /// The MXAccess item handle.
+ ///
public void UnAdvise(int serverHandle, int itemHandle)
{
calls.Add($"UnAdvise:{serverHandle}:{itemHandle}");
}
- /// Records an AdviseSupervisory call.
- /// The MXAccess server handle.
- /// The MXAccess item handle.
+ ///
public void AdviseSupervisory(int serverHandle, int itemHandle)
{
calls.Add($"AdviseSupervisory:{serverHandle}:{itemHandle}");
}
- /// Records a Write call.
- /// The MXAccess server handle.
- /// The MXAccess item handle.
- /// The value to write.
- /// The user identifier.
+ ///
public void Write(int serverHandle, int itemHandle, object? value, int userId)
{
calls.Add($"Write:{serverHandle}:{itemHandle}:{value}:{userId}");
}
- /// Records a Write2 call.
- /// The MXAccess server handle.
- /// The MXAccess item handle.
- /// The value to write.
- /// The timestamp value.
- /// The user identifier.
+ ///
public void Write2(int serverHandle, int itemHandle, object? value, object? timestamp, int userId)
{
calls.Add($"Write2:{serverHandle}:{itemHandle}:{value}:{timestamp}:{userId}");
}
- /// Records a WriteSecured call.
- /// The MXAccess server handle.
- /// The MXAccess item handle.
- /// The current user identifier.
- /// The verifier user identifier.
- /// The value to write.
+ ///
public void WriteSecured(int serverHandle, int itemHandle, int currentUserId, int verifierUserId, object? value)
{
calls.Add($"WriteSecured:{serverHandle}:{itemHandle}:{currentUserId}:{verifierUserId}:{value}");
}
- /// Records a WriteSecured2 call.
- /// The MXAccess server handle.
- /// The MXAccess item handle.
- /// The current user identifier.
- /// The verifier user identifier.
- /// The value to write.
- /// The timestamp value.
+ ///
public void WriteSecured2(
int serverHandle, int itemHandle, int currentUserId, int verifierUserId, object? value, object? timestamp)
{
calls.Add($"WriteSecured2:{serverHandle}:{itemHandle}:{currentUserId}:{verifierUserId}:{value}:{timestamp}");
}
- /// Records a Suspend call and returns a canned status.
- /// The MXAccess server handle.
- /// The MXAccess item handle.
+ ///
public object Suspend(int serverHandle, int itemHandle)
{
calls.Add($"Suspend:{serverHandle}:{itemHandle}");
return new object();
}
- /// Records an Activate call and returns a canned status.
- /// The MXAccess server handle.
- /// The MXAccess item handle.
+ ///
public object Activate(int serverHandle, int itemHandle)
{
calls.Add($"Activate:{serverHandle}:{itemHandle}");
return new object();
}
- /// Records an AuthenticateUser call and returns zero.
- /// The MXAccess server handle.
- /// The user name to authenticate.
- /// The credential; recorded only as a fixed marker, never echoed.
+ ///
public int AuthenticateUser(int serverHandle, string verifyUser, string verifyUserPassword)
{
calls.Add($"AuthenticateUser:{serverHandle}:{verifyUser}");
return 0;
}
- /// Records an ArchestrAUserToId call and returns zero.
- /// The MXAccess server handle.
- /// The ArchestrA user GUID to resolve.
+ ///
public int ArchestrAUserToId(int serverHandle, string userIdGuid)
{
calls.Add($"ArchestrAUserToId:{serverHandle}:{userIdGuid}");
return 0;
}
- /// Records an AddBufferedItem call and returns zero.
- /// The MXAccess server handle.
- /// The item definition string to record.
- /// The item context string to record.
+ ///
public int AddBufferedItem(int serverHandle, string itemDefinition, string itemContext)
{
calls.Add($"AddBufferedItem:{serverHandle}:{itemDefinition}:{itemContext}");
return 0;
}
- /// Records a SetBufferedUpdateInterval call.
- /// The MXAccess server handle.
- /// The buffered update interval in milliseconds.
+ ///
public void SetBufferedUpdateInterval(int serverHandle, int updateIntervalMilliseconds)
{
calls.Add($"SetBufferedUpdateInterval:{serverHandle}:{updateIntervalMilliseconds}");
diff --git a/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/MxAccessCommandExecutorTests.cs b/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/MxAccessCommandExecutorTests.cs
index e48dd19..2b4b0aa 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/MxAccessCommandExecutorTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/MxAccessCommandExecutorTests.cs
@@ -14,6 +14,7 @@ namespace ZB.MOM.WW.MxGateway.Worker.Tests.MxAccess;
public sealed class MxAccessCommandExecutorTests
{
/// Verifies that Register command calls MXAccess on the STA thread and preserves the server handle.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task DispatchAsync_Register_CallsMxAccessOnStaAndPreservesServerHandle()
{
@@ -40,6 +41,7 @@ public sealed class MxAccessCommandExecutorTests
}
/// Verifies that Unregister command calls MXAccess on the STA thread and removes the tracked server handle.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task DispatchAsync_Unregister_CallsMxAccessOnStaAndRemovesTrackedServerHandle()
{
@@ -59,6 +61,7 @@ public sealed class MxAccessCommandExecutorTests
}
/// Verifies that Unregister preserves the HResult when MXAccess throws and does not rewrite the failure.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task DispatchAsync_UnregisterWhenMxAccessThrows_PreservesHResultAndDoesNotRewriteFailure()
{
@@ -86,6 +89,7 @@ public sealed class MxAccessCommandExecutorTests
}
/// Verifies that AddItem command calls MXAccess on the STA thread and tracks the item handle.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task DispatchAsync_AddItem_CallsMxAccessOnStaAndTracksItemHandle()
{
@@ -123,6 +127,7 @@ public sealed class MxAccessCommandExecutorTests
}
/// Verifies that AddItem2 command passes the context exactly and tracks the item handle.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task DispatchAsync_AddItem2_PassesContextExactlyAndTracksItemHandle()
{
@@ -160,6 +165,7 @@ public sealed class MxAccessCommandExecutorTests
}
/// Verifies that RemoveItem command calls MXAccess on the STA thread and removes the tracked item handle.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task DispatchAsync_RemoveItem_CallsMxAccessOnStaAndRemovesTrackedItemHandle()
{
@@ -188,6 +194,7 @@ public sealed class MxAccessCommandExecutorTests
}
/// Verifies that RemoveItem removes tracked advice after MXAccess succeeds on an advised handle.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task DispatchAsync_RemoveItemWithAdvisedHandle_RemovesTrackedAdviceAfterMxAccessSucceeds()
{
@@ -213,6 +220,7 @@ public sealed class MxAccessCommandExecutorTests
}
/// Verifies that RemoveItem preserves the HResult and keeps the tracked item handle when using a cross-server handle.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task DispatchAsync_RemoveItemWithCrossServerHandle_PreservesHResultAndKeepsTrackedItemHandle()
{
@@ -247,6 +255,7 @@ public sealed class MxAccessCommandExecutorTests
}
/// Verifies that AddItem2 preserves the HResult when MXAccess throws and does not track the item handle.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task DispatchAsync_AddItem2WhenMxAccessThrows_PreservesHResultAndDoesNotTrackItemHandle()
{
@@ -276,6 +285,7 @@ public sealed class MxAccessCommandExecutorTests
}
/// Verifies that Advise command calls MXAccess on the STA thread and tracks plain advice.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task DispatchAsync_Advise_CallsMxAccessOnStaAndTracksPlainAdvice()
{
@@ -309,6 +319,7 @@ public sealed class MxAccessCommandExecutorTests
}
/// Verifies that AdviseSupervisory calls a distinct MXAccess method and tracks supervisory advice.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task DispatchAsync_AdviseSupervisory_CallsDistinctMxAccessMethodAndTracksSupervisoryAdvice()
{
@@ -341,6 +352,7 @@ public sealed class MxAccessCommandExecutorTests
}
/// Verifies that UnAdvise command calls MXAccess on the STA thread and removes the tracked advice.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task DispatchAsync_UnAdvise_CallsMxAccessOnStaAndRemovesTrackedAdvice()
{
@@ -369,6 +381,7 @@ public sealed class MxAccessCommandExecutorTests
}
/// Verifies that Advise preserves the HResult when MXAccess throws and does not track the advice.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task DispatchAsync_AdviseWhenMxAccessThrows_PreservesHResultAndDoesNotTrackAdvice()
{
@@ -399,6 +412,7 @@ public sealed class MxAccessCommandExecutorTests
}
/// Verifies that UnAdvise preserves the HResult when MXAccess throws and keeps the tracked advice.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task DispatchAsync_UnAdviseWhenMxAccessThrows_PreservesHResultAndKeepsTrackedAdvice()
{
@@ -433,6 +447,7 @@ public sealed class MxAccessCommandExecutorTests
}
/// Verifies that SubscribeBulk runs sequential MXAccess calls and returns per-item results.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task DispatchAsync_SubscribeBulk_RunsSequentialMxAccessCallsAndReturnsPerItemResults()
{
@@ -478,6 +493,7 @@ public sealed class MxAccessCommandExecutorTests
/// one BulkWriteResult per entry in input order, including a per-entry COM
/// failure surfaced as WasSuccessful=false with the underlying HRESULT.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task DispatchAsync_WriteBulk_RunsSequentialWritesAndReturnsPerEntryResults()
{
@@ -530,6 +546,7 @@ public sealed class MxAccessCommandExecutorTests
}
/// Verifies that Write2Bulk forwards value AND timestamp to each per-entry Write2.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task DispatchAsync_Write2Bulk_ForwardsValueAndTimestampPerEntry()
{
@@ -556,6 +573,7 @@ public sealed class MxAccessCommandExecutorTests
}
/// Verifies that WriteSecuredBulk forwards both user ids per entry.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task DispatchAsync_WriteSecuredBulk_ForwardsUserIdsPerEntry()
{
@@ -580,6 +598,7 @@ public sealed class MxAccessCommandExecutorTests
}
/// Verifies that WriteSecured2Bulk forwards user ids, value, and timestamp per entry.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task DispatchAsync_WriteSecured2Bulk_ForwardsUserIdsValueAndTimestampPerEntry()
{
@@ -612,6 +631,7 @@ public sealed class MxAccessCommandExecutorTests
/// The fake COM object never fires events so the wait always times out — but
/// the lifecycle calls must still happen, in order, on the STA.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task DispatchAsync_ReadBulk_WhenTagNotCached_TakesSnapshotLifecycleAndTimesOut()
{
@@ -650,6 +670,7 @@ public sealed class MxAccessCommandExecutorTests
}
/// Verifies that ReadBulk with no payload returns an invalid request error.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task DispatchAsync_ReadBulkWithoutPayload_ReturnsInvalidRequest()
{
@@ -671,6 +692,7 @@ public sealed class MxAccessCommandExecutorTests
}
/// Verifies that UnsubscribeBulk removes items after UnAdvise failure.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task DispatchAsync_UnsubscribeBulk_RemovesItemAfterUnAdviseFailure()
{
@@ -700,6 +722,7 @@ public sealed class MxAccessCommandExecutorTests
}
/// Verifies that ShutdownGracefullyAsync cleans up handles in advice, item, server order.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task ShutdownGracefullyAsync_CleansHandlesInAdviceItemServerOrder()
{
@@ -725,6 +748,7 @@ public sealed class MxAccessCommandExecutorTests
}
/// Verifies that ShutdownGracefullyAsync records cleanup failures and continues.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task ShutdownGracefullyAsync_RecordsCleanupFailuresAndContinues()
{
@@ -753,6 +777,7 @@ public sealed class MxAccessCommandExecutorTests
}
/// Verifies that Register without payload returns an invalid request error.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task DispatchAsync_RegisterWithoutPayload_ReturnsInvalidRequest()
{
@@ -774,6 +799,7 @@ public sealed class MxAccessCommandExecutorTests
}
/// Verifies that AddItem without payload returns an invalid request error.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task DispatchAsync_AddItemWithoutPayload_ReturnsInvalidRequest()
{
@@ -795,6 +821,7 @@ public sealed class MxAccessCommandExecutorTests
}
/// Verifies that Advise without payload returns an invalid request error.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task DispatchAsync_AdviseWithoutPayload_ReturnsInvalidRequest()
{
@@ -816,6 +843,7 @@ public sealed class MxAccessCommandExecutorTests
}
/// Verifies that Write dispatches the converted value to MXAccess on the STA thread.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task DispatchAsync_Write_CallsMxAccessOnStaWithConvertedValue()
{
@@ -838,6 +866,7 @@ public sealed class MxAccessCommandExecutorTests
}
/// Verifies that Write2 forwards the converted value and timestamp to MXAccess.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task DispatchAsync_Write2_ForwardsValueAndTimestamp()
{
@@ -860,6 +889,7 @@ public sealed class MxAccessCommandExecutorTests
}
/// Verifies that WriteSecured forwards the operator and verifier user ids to MXAccess.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task DispatchAsync_WriteSecured_ForwardsUserIds()
{
@@ -881,6 +911,7 @@ public sealed class MxAccessCommandExecutorTests
}
/// Verifies that WriteSecured2 forwards user ids, value, and timestamp to MXAccess.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task DispatchAsync_WriteSecured2_ForwardsUserIdsValueAndTimestamp()
{
@@ -904,6 +935,7 @@ public sealed class MxAccessCommandExecutorTests
}
/// Verifies that Write without a payload returns an invalid request error.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task DispatchAsync_WriteWithoutPayload_ReturnsInvalidRequest()
{
@@ -926,6 +958,7 @@ public sealed class MxAccessCommandExecutorTests
}
/// Verifies that Write without a value returns an invalid request error.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task DispatchAsync_WriteWithoutValue_ReturnsInvalidRequest()
{
@@ -953,6 +986,7 @@ public sealed class MxAccessCommandExecutorTests
}
/// Verifies Suspend calls MXAccess on the STA and maps the native status to MxStatusProxy.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task DispatchAsync_Suspend_CallsMxAccessOnStaAndMapsStatus()
{
@@ -977,6 +1011,7 @@ public sealed class MxAccessCommandExecutorTests
}
/// Verifies Activate calls MXAccess on the STA and maps the native status to MxStatusProxy.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task DispatchAsync_Activate_CallsMxAccessOnStaAndMapsStatus()
{
@@ -1000,6 +1035,7 @@ public sealed class MxAccessCommandExecutorTests
}
/// Verifies AuthenticateUser passes credentials to MXAccess on the STA and returns the user id.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task DispatchAsync_AuthenticateUser_CallsMxAccessOnStaAndReturnsUserId()
{
@@ -1026,6 +1062,7 @@ public sealed class MxAccessCommandExecutorTests
/// command reply or any recorded diagnostic — the password is only ever
/// handed straight to the MXAccess wrapper.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task DispatchAsync_AuthenticateUser_DoesNotLeakPassword()
{
@@ -1051,6 +1088,7 @@ public sealed class MxAccessCommandExecutorTests
}
/// Verifies ArchestrAUserToId calls MXAccess on the STA and returns the resolved user id.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task DispatchAsync_ArchestrAUserToId_CallsMxAccessOnStaAndReturnsUserId()
{
@@ -1072,6 +1110,7 @@ public sealed class MxAccessCommandExecutorTests
}
/// Verifies AddBufferedItem calls MXAccess on the STA and tracks the buffered item handle.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task DispatchAsync_AddBufferedItem_CallsMxAccessOnStaAndTracksItemHandle()
{
@@ -1104,6 +1143,7 @@ public sealed class MxAccessCommandExecutorTests
}
/// Verifies SetBufferedUpdateInterval calls MXAccess on the STA and returns a base OK reply.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task DispatchAsync_SetBufferedUpdateInterval_CallsMxAccessOnStaAndReturnsOk()
{
@@ -1131,6 +1171,7 @@ public sealed class MxAccessCommandExecutorTests
/// throw would propagate an unhandled exception through WorkerPipeSession
/// and no other test would catch it.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task DispatchAsync_WithUnknownCommandKind_ReturnsInvalidRequestWithUnsupportedDiagnostic()
{
@@ -1860,9 +1901,7 @@ public sealed class MxAccessCommandExecutorTests
/// Gets the list of operations performed on this fake object.
public IReadOnlyList OperationNames => operationNames.ToArray();
- /// Registers a client and returns a server handle.
- /// Name of the client to register.
- /// The server handle for the registered client.
+ ///
public int Register(string clientName)
{
operationNames.Add($"Register:{clientName}");
@@ -1872,8 +1911,7 @@ public sealed class MxAccessCommandExecutorTests
return registerHandle;
}
- /// Unregisters a server and tracks the operation.
- /// Server handle to unregister.
+ ///
public void Unregister(int serverHandle)
{
operationNames.Add($"Unregister:{serverHandle}");
@@ -1886,10 +1924,7 @@ public sealed class MxAccessCommandExecutorTests
}
}
- /// Adds an item to the server and returns its item handle.
- /// Server handle to add the item to.
- /// Item definition string.
- /// The item handle for the added item.
+ ///
public int AddItem(
int serverHandle,
string itemDefinition)
@@ -1907,11 +1942,7 @@ public sealed class MxAccessCommandExecutorTests
return addItemHandle;
}
- /// Adds an item to the server with context and returns its item handle.
- /// Server handle to add the item to.
- /// Item definition string.
- /// Item context string.
- /// The item handle for the added item.
+ ///
public int AddItem2(
int serverHandle,
string itemDefinition,
@@ -1931,9 +1962,7 @@ public sealed class MxAccessCommandExecutorTests
return addItem2Handle;
}
- /// Removes an item from the server and tracks the operation.
- /// Server handle from which to remove the item.
- /// Item handle to remove.
+ ///
public void RemoveItem(
int serverHandle,
int itemHandle)
@@ -1949,9 +1978,7 @@ public sealed class MxAccessCommandExecutorTests
}
}
- /// Advises on item changes and tracks the operation.
- /// Server handle for the advisory subscription.
- /// Item handle to advise on.
+ ///
public void Advise(
int serverHandle,
int itemHandle)
@@ -1967,9 +1994,7 @@ public sealed class MxAccessCommandExecutorTests
}
}
- /// Removes an item advice subscription and tracks the operation.
- /// Server handle from which to remove the subscription.
- /// Item handle to remove advice from.
+ ///
public void UnAdvise(
int serverHandle,
int itemHandle)
@@ -1985,9 +2010,7 @@ public sealed class MxAccessCommandExecutorTests
}
}
- /// Advises supervisory on item changes and tracks the operation.
- /// Server handle for the supervisory subscription.
- /// Item handle to advise supervisory on.
+ ///
public void AdviseSupervisory(
int serverHandle,
int itemHandle)
@@ -2027,11 +2050,7 @@ public sealed class MxAccessCommandExecutorTests
/// Gets the thread ID on which the most recent write was called.
public int? WriteThreadId { get; private set; }
- /// Writes a value to an item and tracks the operation.
- /// Server handle for the write.
- /// Item handle to write to.
- /// Value to write.
- /// MXAccess user id for the write.
+ ///
public void Write(
int serverHandle,
int itemHandle,
@@ -2047,12 +2066,7 @@ public sealed class MxAccessCommandExecutorTests
ThrowIfWriteFailureConfigured(itemHandle);
}
- /// Writes a timestamped value to an item and tracks the operation.
- /// Server handle for the write.
- /// Item handle to write to.
- /// Value to write.
- /// Source timestamp for the write.
- /// MXAccess user id for the write.
+ ///
public void Write2(
int serverHandle,
int itemHandle,
@@ -2070,12 +2084,7 @@ public sealed class MxAccessCommandExecutorTests
ThrowIfWriteFailureConfigured(itemHandle);
}
- /// Performs a secured write to an item and tracks the operation.
- /// Server handle for the write.
- /// Item handle to write to.
- /// Operator user id.
- /// Verifier user id.
- /// Value to write.
+ ///
public void WriteSecured(
int serverHandle,
int itemHandle,
@@ -2093,13 +2102,7 @@ public sealed class MxAccessCommandExecutorTests
ThrowIfWriteFailureConfigured(itemHandle);
}
- /// Performs a secured timestamped write to an item and tracks the operation.
- /// Server handle for the write.
- /// Item handle to write to.
- /// Operator user id.
- /// Verifier user id.
- /// Value to write.
- /// Source timestamp for the write.
+ ///
public void WriteSecured2(
int serverHandle,
int itemHandle,
@@ -2181,10 +2184,7 @@ public sealed class MxAccessCommandExecutorTests
/// Gets the interval passed to SetBufferedUpdateInterval, if called.
public int? SetBufferedUpdateIntervalValue { get; private set; }
- /// Suspends an item and returns a canned status whose fields drive MxStatusProxy conversion.
- /// Server handle for the suspend.
- /// Item handle to suspend.
- /// A status stand-in with all-OK fields.
+ ///
public object Suspend(int serverHandle, int itemHandle)
{
operationNames.Add($"Suspend:{serverHandle}:{itemHandle}");
@@ -2194,10 +2194,7 @@ public sealed class MxAccessCommandExecutorTests
return new FakeMxStatus { success = 1, category = 0, detectedBy = 0, detail = 0 };
}
- /// Activates an item and returns a canned status whose fields drive MxStatusProxy conversion.
- /// Server handle for the activate.
- /// Item handle to activate.
- /// A status stand-in with all-OK fields.
+ ///
public object Activate(int serverHandle, int itemHandle)
{
operationNames.Add($"Activate:{serverHandle}:{itemHandle}");
@@ -2207,11 +2204,7 @@ public sealed class MxAccessCommandExecutorTests
return new FakeMxStatus { success = 1, category = 0, detectedBy = 0, detail = 0 };
}
- /// Authenticates a user and returns a canned user id.
- /// Server handle for the authentication.
- /// User name to authenticate.
- /// Credential; recorded only to assert it is never logged.
- /// The canned MXAccess user id (1).
+ ///
public int AuthenticateUser(int serverHandle, string verifyUser, string verifyUserPassword)
{
// Deliberately does NOT include the password in the operation log.
@@ -2223,10 +2216,7 @@ public sealed class MxAccessCommandExecutorTests
return 1;
}
- /// Resolves an ArchestrA user GUID and returns a canned user id.
- /// Server handle for the resolution.
- /// ArchestrA user GUID to resolve.
- /// The canned MXAccess user id (7).
+ ///
public int ArchestrAUserToId(int serverHandle, string userIdGuid)
{
operationNames.Add($"ArchestrAUserToId:{serverHandle}:{userIdGuid}");
@@ -2235,11 +2225,7 @@ public sealed class MxAccessCommandExecutorTests
return 7;
}
- /// Adds a buffered item and returns a canned item handle.
- /// Server handle to add the item to.
- /// Item definition string.
- /// Item context string.
- /// The canned buffered item handle (1).
+ ///
public int AddBufferedItem(int serverHandle, string itemDefinition, string itemContext)
{
operationNames.Add($"AddBufferedItem:{serverHandle}:{itemDefinition}:{itemContext}");
@@ -2249,9 +2235,7 @@ public sealed class MxAccessCommandExecutorTests
return 1;
}
- /// Sets the buffered update interval and tracks the operation.
- /// Server handle for the interval change.
- /// Buffered update interval in milliseconds.
+ ///
public void SetBufferedUpdateInterval(int serverHandle, int updateIntervalMilliseconds)
{
operationNames.Add($"SetBufferedUpdateInterval:{serverHandle}:{updateIntervalMilliseconds}");
@@ -2274,8 +2258,7 @@ public sealed class MxAccessCommandExecutorTests
/// Gets the fake COM object.
public FakeMxAccessComObject FakeComObject { get; }
- /// Creates and returns the fake MXAccess COM object.
- /// The fake COM object.
+ ///
public object Create()
{
return FakeComObject;
diff --git a/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/MxAccessLiveComCreationTests.cs b/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/MxAccessLiveComCreationTests.cs
index 9479fb1..404a8fd 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/MxAccessLiveComCreationTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/MxAccessLiveComCreationTests.cs
@@ -15,6 +15,7 @@ public sealed class MxAccessLiveComCreationTests
private const string DefaultLiveAddItem2Context = "TestChildObject";
/// Verifies that StartAsync creates the installed MXAccess COM object on the STA thread when opted in.
+ /// A task that represents the asynchronous operation.
[LiveMxAccessFact]
public async Task StartAsync_WhenOptedIn_CreatesInstalledMxAccessComObjectOnSta()
{
@@ -24,6 +25,7 @@ public sealed class MxAccessLiveComCreationTests
}
/// Verifies that Register and Unregister round-trip server handles with installed MXAccess.
+ /// A task that represents the asynchronous operation.
[LiveMxAccessFact]
public async Task RegisterAndUnregister_WhenOptedIn_RoundTripsInstalledMxAccessServerHandle()
{
@@ -61,6 +63,7 @@ public sealed class MxAccessLiveComCreationTests
}
/// Verifies that AddItem and RemoveItem round-trip item handles with installed MXAccess.
+ /// A task that represents the asynchronous operation.
[LiveMxAccessFact]
public async Task AddItemAndRemoveItem_WhenOptedIn_RoundTripsInstalledMxAccessItemHandle()
{
@@ -129,6 +132,7 @@ public sealed class MxAccessLiveComCreationTests
}
/// Verifies that AddItem2 and RemoveItem preserve item context with installed MXAccess.
+ /// A task that represents the asynchronous operation.
[LiveMxAccessFact]
public async Task AddItem2AndRemoveItem_WhenOptedIn_PreservesContextForInstalledMxAccess()
{
@@ -198,6 +202,7 @@ public sealed class MxAccessLiveComCreationTests
}
/// Verifies that Advise and UnAdvise round-trip subscriptions with installed MXAccess.
+ /// A task that represents the asynchronous operation.
[LiveMxAccessFact]
public async Task AdviseAndUnAdvise_WhenOptedIn_RoundTripsInstalledMxAccessSubscription()
{
diff --git a/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/MxAccessStaSessionTests.cs b/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/MxAccessStaSessionTests.cs
index 7482041..ffa47f9 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/MxAccessStaSessionTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/MxAccessStaSessionTests.cs
@@ -18,6 +18,7 @@ public sealed class MxAccessStaSessionTests
///
/// Verifies that StartAsync creates the MXAccess COM object and attaches the event sink on the STA thread.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task StartAsync_CreatesComObjectAndAttachesEventSinkOnStaThread()
{
@@ -42,6 +43,7 @@ public sealed class MxAccessStaSessionTests
///
/// Verifies that StartAsync maps creation exceptions with HResult when the factory fails.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task StartAsync_WhenFactoryFails_MapsCreationExceptionWithHResult()
{
@@ -63,6 +65,7 @@ public sealed class MxAccessStaSessionTests
///
/// Verifies that Dispose detaches the event sink on the STA thread.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task Dispose_DetachesEventSinkOnStaThread()
{
@@ -116,9 +119,7 @@ public sealed class MxAccessStaSessionTests
///
public ApartmentState? CreateApartmentState { get; private set; }
- ///
- /// Creates the COM object or throws the configured exception.
- ///
+ ///
public object Create()
{
CreateThreadId = Thread.CurrentThread.ManagedThreadId;
@@ -158,11 +159,7 @@ public sealed class MxAccessStaSessionTests
///
public string? SessionId { get; private set; }
- ///
- /// Attaches the MXAccess COM object and records thread context.
- ///
- /// MXAccess COM object to attach.
- /// Identifier of the session.
+ ///
public void Attach(
object mxAccessComObject,
string sessionId)
@@ -172,9 +169,7 @@ public sealed class MxAccessStaSessionTests
SessionId = sessionId;
}
- ///
- /// Detaches the MXAccess COM object and records thread context.
- ///
+ ///
public void Detach()
{
DetachThreadId = Thread.CurrentThread.ManagedThreadId;
@@ -188,6 +183,7 @@ public sealed class MxAccessStaSessionTests
/// This proves the fix in WorkerPipeSession (and the new internal constructor) correctly
/// wires the factory rather than leaving alarmCommandHandler null.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task StartAsync_WithAlarmCommandHandlerFactory_SubscribeAlarmsCommandReachesHandler()
{
@@ -231,6 +227,7 @@ public sealed class MxAccessStaSessionTests
/// test fails if the diagnostic regresses to a misleading message that still
/// happens to contain the word "alarm".
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task StartAsync_WithoutAlarmCommandHandlerFactory_SubscribeAlarmsReturnsInvalidRequest()
{
@@ -267,6 +264,7 @@ public sealed class MxAccessStaSessionTests
/// loop calls PollOnce on the handler via the STA within a reasonable timeout.
/// This proves polling is driven by the STA rather than the consumer's internal timer.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task StartAsync_WithAlarmCommandHandlerFactory_PollOnceCalledViaSta()
{
@@ -304,6 +302,7 @@ public sealed class MxAccessStaSessionTests
/// immediately after Dispose and stays frozen — deterministic, with no
/// elapsed-time "no further polls" window that a slow agent could race.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task Dispose_StopsAlarmPollLoop()
{
@@ -346,12 +345,13 @@ public sealed class MxAccessStaSessionTests
}
///
- /// Worker-005 regression: when the alarm poll loop's PollOnce throws a
+ /// When the alarm poll loop's PollOnce throws a
/// real failure (e.g. a COMException from GetXmlCurrentAlarms2), the
/// failure must be recorded as a fault on the event queue so a broken
/// alarm subscription becomes observable on the IPC fault path instead
/// of silently faulting the never-awaited poll task.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task RunAlarmPollLoop_WhenPollOnceThrows_RecordsFaultOnEventQueue()
{
@@ -389,7 +389,7 @@ public sealed class MxAccessStaSessionTests
}
///
- /// Worker-016 regression: the alarm poll loop's catch for the graceful
+ /// The alarm poll loop's catch for the graceful
/// STA-runtime-shutdown signal must NOT also swallow a vanilla
/// raised from inside the marshalled
/// poll lambda — for example the STA-affinity assertion thrown by
@@ -399,6 +399,7 @@ public sealed class MxAccessStaSessionTests
/// from PollOnce must reach
/// the fault-recording arm and become observable on the event queue.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task RunAlarmPollLoop_WhenPollOnceThrowsInvalidOperation_RecordsFaultOnEventQueue()
{
@@ -438,7 +439,7 @@ public sealed class MxAccessStaSessionTests
}
///
- /// Worker-008 regression: the STA-affinity guard throws when an
+ /// The STA-affinity guard throws when an
/// IMxAccessAlarmConsumer call is attempted off the thread that created
/// the consumer, mirroring the MxAccessSession.CreationThreadId invariant.
///
@@ -455,7 +456,7 @@ public sealed class MxAccessStaSessionTests
}
///
- /// Worker-008: the STA-affinity guard is a no-op on the owning thread and
+ /// The STA-affinity guard is a no-op on the owning thread and
/// when no alarm consumer is configured (expected thread id null).
///
[Fact]
@@ -495,52 +496,35 @@ public sealed class MxAccessStaSessionTests
get { lock (gate) return lastPollThreadId; }
}
- /// Subscribes to alarm events.
- /// The subscribe-alarms command.
- /// The session identifier.
+ ///
public void Subscribe(SubscribeAlarmsCommand command, string sessionId)
{
IsSubscribed = true;
LastSubscription = command.SubscriptionExpression;
}
- /// Unsubscribes from alarm events.
+ ///
public void Unsubscribe()
{
IsSubscribed = false;
}
- /// Acknowledges an alarm by guid.
- /// The alarm GUID.
- /// The acknowledgment comment.
- /// The operator user name.
- /// The operator node name.
- /// The operator domain.
- /// The operator full name.
+ ///
public int Acknowledge(Guid alarmGuid, string comment, string operatorUser,
string operatorNode, string operatorDomain, string operatorFullName)
=> 0;
- /// Acknowledges an alarm by name.
- /// The alarm name.
- /// The provider name.
- /// The alarm group name.
- /// The acknowledgment comment.
- /// The operator user name.
- /// The operator node name.
- /// The operator domain.
- /// The operator full name.
+ ///
public int AcknowledgeByName(string alarmName, string providerName, string groupName,
string comment, string operatorUser, string operatorNode,
string operatorDomain, string operatorFullName)
=> 0;
- /// Queries active alarms.
- /// Optional alarm name filter prefix.
+ ///
public IReadOnlyList QueryActive(string? alarmFilterPrefix)
=> Array.Empty();
- /// Polls for alarm events once.
+ ///
public void PollOnce()
{
lock (gate)
diff --git a/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/MxAccessValueCacheTests.cs b/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/MxAccessValueCacheTests.cs
index 14b2a8e..7e5cb4f 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/MxAccessValueCacheTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/MxAccessValueCacheTests.cs
@@ -115,6 +115,7 @@ public sealed class MxAccessValueCacheTests
}
/// Verifies that TryWaitForUpdate returns true when the cache is updated after the baseline.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task TryWaitForUpdate_ReturnsTrue_WhenSetFiresAfterBaselineVersion()
{
diff --git a/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/SubtagAlarmConsumerTests.cs b/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/SubtagAlarmConsumerTests.cs
index 572aec3..596ae9a 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/SubtagAlarmConsumerTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/SubtagAlarmConsumerTests.cs
@@ -294,16 +294,13 @@ public sealed class SubtagAlarmConsumerTests
/// Gets the most recent (address, value) pair passed to .
public (string Address, object? Value)? LastWrite { get; private set; }
- /// Records the advised subtag addresses.
- /// The subtag references to advise.
+ ///
public void Advise(IReadOnlyCollection itemAddresses)
{
Advised.AddRange(itemAddresses);
}
- /// Records the most recent write.
- /// The subtag reference to write.
- /// The value to write.
+ ///
public void Write(string itemAddress, object? value)
{
LastWrite = (itemAddress, value);
diff --git a/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/SubtagAlarmStateMachineTests.cs b/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/SubtagAlarmStateMachineTests.cs
index 6f1ee53..c677340 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/SubtagAlarmStateMachineTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/SubtagAlarmStateMachineTests.cs
@@ -23,6 +23,7 @@ public sealed class SubtagAlarmStateMachineTests
AckCommentSubtag = "Tank01.Level.HiHi.ackmsg",
};
+ /// Verifies that an active transition from false to true emits an unacked raise.
[Fact]
public void ActiveFalseToTrue_EmitsRaise()
{
@@ -37,6 +38,7 @@ public sealed class SubtagAlarmStateMachineTests
Assert.Equal("Area", e.Record.Group);
}
+ /// Verifies that a subtag target built from a real Galaxy area round-trips to native alarmmgr provider/group/tag-name fields.
[Fact]
public void ActiveFalseToTrue_AlarmMgrShape_EmitsNativeProviderGroupTagName()
{
@@ -59,6 +61,7 @@ public sealed class SubtagAlarmStateMachineTests
Assert.Equal("TestMachine_001.TestAlarm001", e.Record.TagName);
}
+ /// Verifies that when the alarm reference has no provider "!" separator, the whole reference is used as the tag name.
[Fact]
public void ActiveFalseToTrue_NoProviderBang_UsesWholeReferenceAsTagName()
{
@@ -77,6 +80,7 @@ public sealed class SubtagAlarmStateMachineTests
Assert.Equal(string.Empty, e.Record.Group);
}
+ /// Verifies that an out-of-order un-ack arriving before the active-false clear still results in an AckRtn transition.
[Fact]
public void OutOfOrderAckThenClear_StillEmitsAckRtn()
{
@@ -91,6 +95,7 @@ public sealed class SubtagAlarmStateMachineTests
Assert.Equal(MxAlarmStateKind.AckRtn, e.Record.State);
}
+ /// Verifies that constructing the state machine with two targets sharing an active subtag address throws.
[Fact]
public void DuplicateActiveSubtag_Throws()
{
@@ -110,7 +115,7 @@ public sealed class SubtagAlarmStateMachineTests
}
///
- /// Worker-028 regression: two watch-list entries sharing an
+ /// Two watch-list entries sharing an
/// (but using distinct
/// subtag addresses) must throw at construction, symmetric with the
/// duplicate-address guard, rather than silently overwriting the earlier
@@ -134,6 +139,7 @@ public sealed class SubtagAlarmStateMachineTests
Assert.Throws(() => new SubtagAlarmStateMachine(new[] { first, second }));
}
+ /// Verifies that acking an active, unacked alarm emits an AckAlm transition from UnackAlm.
[Fact]
public void AckedTrueWhileActive_EmitsAck()
{
@@ -146,6 +152,7 @@ public sealed class SubtagAlarmStateMachineTests
Assert.Equal(MxAlarmStateKind.UnackAlm, e.PreviousState);
}
+ /// Verifies that clearing an active, unacked alarm emits an UnackRtn transition.
[Fact]
public void ActiveTrueToFalse_WhileUnacked_EmitsUnackRtn()
{
@@ -157,6 +164,7 @@ public sealed class SubtagAlarmStateMachineTests
Assert.Equal(MxAlarmStateKind.UnackRtn, e.Record.State);
}
+ /// Verifies that clearing an active, acked alarm emits an AckRtn transition.
[Fact]
public void ActiveTrueToFalse_WhileAcked_EmitsAckRtn()
{
@@ -169,6 +177,7 @@ public sealed class SubtagAlarmStateMachineTests
Assert.Equal(MxAlarmStateKind.AckRtn, e.Record.State);
}
+ /// Verifies that SnapshotActive reflects the current active and acked state.
[Fact]
public void Snapshot_ReflectsActiveAndAckedState()
{
@@ -180,6 +189,7 @@ public sealed class SubtagAlarmStateMachineTests
Assert.Equal(MxAlarmStateKind.AckAlm, snap.State);
}
+ /// Verifies that a value change on an address not bound to any target produces no events.
[Fact]
public void UnknownAddress_NoEvents()
{
@@ -189,7 +199,7 @@ public sealed class SubtagAlarmStateMachineTests
}
///
- /// Worker.Tests-033 regression: an ack arriving while the alarm is NOT
+ /// An ack arriving while the alarm is NOT
/// active must emit nothing and must NOT latch
/// AckedDuringEpisode — otherwise a stale ack from a prior episode
/// would mis-latch the next raise into a spurious ACK_RTN on clear. The
@@ -214,7 +224,7 @@ public sealed class SubtagAlarmStateMachineTests
}
///
- /// Worker.Tests-033 regression: a priority-subtag value change must flow
+ /// A priority-subtag value change must flow
/// through CoerceInt into the emitted record's
/// . A non-numeric value must
/// leave the prior priority unchanged (the CoerceInt fallback path).
diff --git a/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/SyntheticAlarmGuidTests.cs b/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/SyntheticAlarmGuidTests.cs
index f0a1021..9280757 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/SyntheticAlarmGuidTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/SyntheticAlarmGuidTests.cs
@@ -35,7 +35,7 @@ public sealed class SyntheticAlarmGuidTests
Assert.NotEqual(Guid.Empty, SyntheticAlarmGuid.ForReference(string.Empty));
///
- /// Worker-027 regression:
+ /// Regression test:
/// must derive its GUID without routing through
/// , because on net48
/// MD5.Create() throws under the Windows FIPS-compliance policy.
diff --git a/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/WnWrapAlarmConsumerXmlTests.cs b/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/WnWrapAlarmConsumerXmlTests.cs
index 32197b6..7a455c2 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/WnWrapAlarmConsumerXmlTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/WnWrapAlarmConsumerXmlTests.cs
@@ -125,7 +125,7 @@ public sealed class WnWrapAlarmConsumerXmlTests
}
///
- /// Worker-001 regression: the consumer must own no internal
+ /// The consumer must own no internal
/// . A thread-pool timer calling the
/// apartment-threaded wnwrap COM object off its owning STA can
/// deadlock on cross-apartment marshaling, so the timer field and
@@ -144,7 +144,7 @@ public sealed class WnWrapAlarmConsumerXmlTests
}
///
- /// Worker-001 regression: no public constructor may accept a
+ /// No public constructor may accept a
/// poll-interval parameter. A non-zero poll interval was the only
/// way to arm the off-STA timer; removing the parameter makes the
/// footgun structurally unreachable.
@@ -163,7 +163,7 @@ public sealed class WnWrapAlarmConsumerXmlTests
}
///
- /// Worker.Tests-022: pins the "new alarm sighting" branch of
+ /// Pins the "new alarm sighting" branch of
/// . A GUID
/// that appears in next but not in previous must
/// produce exactly one transition with
@@ -191,7 +191,7 @@ public sealed class WnWrapAlarmConsumerXmlTests
}
///
- /// Worker.Tests-022: pins the "state unchanged" branch. A GUID
+ /// Pins the "state unchanged" branch. A GUID
/// present in both snapshots with identical
/// must produce no
/// transition — a regression that emits a transition every poll
@@ -218,7 +218,7 @@ public sealed class WnWrapAlarmConsumerXmlTests
}
///
- /// Worker.Tests-022: pins the "state changed" branch. A GUID
+ /// Pins the "state changed" branch. A GUID
/// present in both snapshots with a different state must produce
/// one transition carrying the prior state so the proto layer
/// can distinguish e.g. UnackAlm→AckAlm
@@ -247,7 +247,7 @@ public sealed class WnWrapAlarmConsumerXmlTests
}
///
- /// Worker.Tests-022: pins the "alarm cleared from the active set"
+ /// Pins the "alarm cleared from the active set"
/// branch. AVEVA drops cleared alarms from
/// GetXmlCurrentAlarms2's active set rather than emitting a
/// transition record. A GUID present in
@@ -273,7 +273,7 @@ public sealed class WnWrapAlarmConsumerXmlTests
}
///
- /// Worker.Tests-022: pins the multi-alarm fan-out. Multiple
+ /// Pins the multi-alarm fan-out. Multiple
/// simultaneous transitions (new + changed + unchanged + dropped)
/// in one snapshot must produce exactly the changed and new
/// entries — not the unchanged and not the dropped.
diff --git a/src/ZB.MOM.WW.MxGateway.Worker.Tests/Probes/AlarmSubtagLiveSmokeTests.cs b/src/ZB.MOM.WW.MxGateway.Worker.Tests/Probes/AlarmSubtagLiveSmokeTests.cs
index ce9b3ac..aa3857c 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker.Tests/Probes/AlarmSubtagLiveSmokeTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker.Tests/Probes/AlarmSubtagLiveSmokeTests.cs
@@ -555,6 +555,13 @@ public sealed class AlarmSubtagLiveSmokeTests
internal int ptY;
}
+ /// Checks the thread message queue for a message, optionally removing it.
+ /// Receives the retrieved message.
+ /// Window handle to filter messages for, or for any window owned by the calling thread.
+ /// Lowest message value to examine.
+ /// Highest message value to examine.
+ /// Specifies how the message is handled (e.g. ).
+ /// if a message is available; otherwise .
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool PeekMessage(
@@ -564,10 +571,16 @@ public sealed class AlarmSubtagLiveSmokeTests
uint wMsgFilterMax,
uint wRemoveMsg);
+ /// Translates virtual-key messages into character messages.
+ /// The message to translate.
+ /// if the message was translated; otherwise .
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool TranslateMessage(ref MSG lpMsg);
+ /// Dispatches a message to the window procedure.
+ /// The message to dispatch.
+ /// The value returned by the window procedure.
[DllImport("user32.dll")]
internal static extern IntPtr DispatchMessage(ref MSG lpmsg);
}
diff --git a/src/ZB.MOM.WW.MxGateway.Worker.Tests/Probes/WnWrapConsumerProbeTests.cs b/src/ZB.MOM.WW.MxGateway.Worker.Tests/Probes/WnWrapConsumerProbeTests.cs
index ee99011..0f3b4f0 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker.Tests/Probes/WnWrapConsumerProbeTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker.Tests/Probes/WnWrapConsumerProbeTests.cs
@@ -27,7 +27,7 @@ public sealed class WnWrapConsumerProbeTests
private static readonly string SubscriptionExpression =
$@"\\{MachineName}\Galaxy!DEV";
- // XML query form — per WIN-911 / ArchestrA reference. NODE is the
+ // XML query form — per ArchestrA reference. NODE is the
// machine, PROVIDER is the literal "Galaxy", GROUP is the area.
private static readonly string XmlAlarmQuery =
"" +
diff --git a/src/ZB.MOM.WW.MxGateway.Worker.Tests/Sta/StaCommandDispatcherTests.cs b/src/ZB.MOM.WW.MxGateway.Worker.Tests/Sta/StaCommandDispatcherTests.cs
index 7987190..ee189c6 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker.Tests/Sta/StaCommandDispatcherTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker.Tests/Sta/StaCommandDispatcherTests.cs
@@ -18,6 +18,7 @@ public sealed class StaCommandDispatcherTests
///
/// Verifies commands execute on the STA thread in queue order.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task DispatchAsync_ExecutesCommandsOnStaInQueueOrder()
{
@@ -41,6 +42,7 @@ public sealed class StaCommandDispatcherTests
///
/// Verifies executor exceptions are captured as HResult in the reply without exposing message details.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task DispatchAsync_WhenExecutorThrows_ReturnsFailureReplyWithHResult()
{
@@ -64,6 +66,7 @@ public sealed class StaCommandDispatcherTests
///
/// Verifies cancellation before execution prevents the command from running.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task DispatchAsync_WhenCanceledBeforeExecution_ReturnsCanceledReplyWithoutExecuting()
{
@@ -96,6 +99,7 @@ public sealed class StaCommandDispatcherTests
/// observed and ignored" from "cancel never checked"; it only proves the
/// in-flight command is not aborted.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task DispatchAsync_WhenCanceledWhileExecuting_DoesNotAbortInFlightCommand()
{
@@ -121,6 +125,7 @@ public sealed class StaCommandDispatcherTests
///
/// Verifies shutdown rejects new dispatch attempts.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task DispatchAsync_WhenShutdownRequested_RejectsNewCommands()
{
@@ -138,6 +143,7 @@ public sealed class StaCommandDispatcherTests
///
/// Verifies shutdown allows the current command to complete but rejects queued commands.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task RequestShutdown_RejectsQueuedCommandButLetsCurrentCommandFinish()
{
@@ -162,6 +168,7 @@ public sealed class StaCommandDispatcherTests
///
/// Verifies heartbeat reports current command correlation ID and pending command count.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task PopulateHeartbeat_ReportsCurrentCorrelationAndPendingCount()
{
diff --git a/src/ZB.MOM.WW.MxGateway.Worker.Tests/Sta/StaMessagePumpTests.cs b/src/ZB.MOM.WW.MxGateway.Worker.Tests/Sta/StaMessagePumpTests.cs
index b9eeba8..3f4e30a 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker.Tests/Sta/StaMessagePumpTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker.Tests/Sta/StaMessagePumpTests.cs
@@ -37,6 +37,7 @@ public sealed class StaMessagePumpTests
///
/// Verifies that WaitForWorkOrMessages returns promptly when the wake event is already signalled.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task WaitForWorkOrMessages_WakeEventAlreadySignalled_ReturnsImmediately()
{
@@ -60,6 +61,7 @@ public sealed class StaMessagePumpTests
///
/// Verifies that WaitForWorkOrMessages wakes when the wake event is signalled from another thread.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task WaitForWorkOrMessages_WakeEventSignalledDuringWait_Returns()
{
@@ -89,6 +91,7 @@ public sealed class StaMessagePumpTests
///
/// Verifies that WaitForWorkOrMessages returns on timeout when the wake event is never signalled.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task WaitForWorkOrMessages_WakeEventNeverSignalled_ReturnsAfterTimeout()
{
@@ -113,6 +116,7 @@ public sealed class StaMessagePumpTests
///
/// Verifies that a zero timeout (the TimeSpan.Zero conversion branch) returns without blocking.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task WaitForWorkOrMessages_ZeroTimeout_ReturnsWithoutBlocking()
{
@@ -137,6 +141,7 @@ public sealed class StaMessagePumpTests
///
/// Verifies that PumpPendingMessages returns zero when the STA thread message queue is empty.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task PumpPendingMessages_NoMessagesPosted_ReturnsZero()
{
@@ -155,6 +160,7 @@ public sealed class StaMessagePumpTests
///
/// Verifies that PumpPendingMessages dispatches and counts messages posted to the STA thread.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task PumpPendingMessages_MessagesPostedToStaThread_ReturnsCountProcessed()
{
@@ -179,6 +185,7 @@ public sealed class StaMessagePumpTests
///
/// Verifies that WaitForWorkOrMessages returns once a Windows message is posted to the STA thread.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task WaitForWorkOrMessages_WindowsMessagePosted_ReturnsForInputAvailable()
{
diff --git a/src/ZB.MOM.WW.MxGateway.Worker.Tests/Sta/StaRuntimeTests.cs b/src/ZB.MOM.WW.MxGateway.Worker.Tests/Sta/StaRuntimeTests.cs
index ebccf33..3afaa85 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker.Tests/Sta/StaRuntimeTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker.Tests/Sta/StaRuntimeTests.cs
@@ -8,6 +8,7 @@ namespace ZB.MOM.WW.MxGateway.Worker.Tests.Sta;
public sealed class StaRuntimeTests
{
/// Verifies that InvokeAsync executes commands on the STA thread.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task InvokeAsync_ExecutesCommandOnStaThread()
{
@@ -35,6 +36,7 @@ public sealed class StaRuntimeTests
/// correct dispatch past an arbitrary millisecond budget, which would be a
/// false failure.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task InvokeAsync_WakesIdlePumpForQueuedCommand()
{
@@ -84,6 +86,7 @@ public sealed class StaRuntimeTests
}
/// Verifies that InvokeAsync faults the returned task when a command raises an exception without stopping the runtime.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task InvokeAsync_CommandException_FaultsReturnedTaskWithoutStoppingRuntime()
{
@@ -101,14 +104,15 @@ public sealed class StaRuntimeTests
///
/// Verifies that InvokeAsync returns a faulted task when called after
- /// Shutdown. Worker-016 introduced
- /// (a dedicated subtype of ) so
+ /// Shutdown. is
+ /// a dedicated subtype of so
/// callers — notably MxAccessStaSession.RunAlarmPollLoopAsync —
/// can distinguish the graceful shutdown signal from a vanilla
/// such as an STA-affinity
/// assertion. The test pins the exact type so a regression that
/// reverts to a plain InvalidOperationException fails here.
///
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task InvokeAsync_AfterShutdown_ReturnsFaultedTask()
{
@@ -164,14 +168,14 @@ public sealed class StaRuntimeTests
/// The thread ID where Uninitialize was called.
public int? UninitializeThreadId { get; private set; }
- /// Initializes the COM apartment and records the calling thread.
+ ///
public void Initialize()
{
InitializeCount++;
InitializeThreadId = Thread.CurrentThread.ManagedThreadId;
}
- /// Uninitializes the COM apartment and records the calling thread.
+ ///
public void Uninitialize()
{
UninitializeCount++;
diff --git a/src/ZB.MOM.WW.MxGateway.Worker.Tests/TestSupport/FakeRuntimeSession.cs b/src/ZB.MOM.WW.MxGateway.Worker.Tests/TestSupport/FakeRuntimeSession.cs
index 7bd6ea7..c1002a9 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker.Tests/TestSupport/FakeRuntimeSession.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker.Tests/TestSupport/FakeRuntimeSession.cs
@@ -46,11 +46,7 @@ internal sealed class FakeRuntimeSession : IWorkerRuntimeSession
/// Gets a value indicating whether Dispose was called.
public bool Disposed { get; private set; }
- /// Starts the worker session with the given session ID and process ID.
- /// The session identifier.
- /// The worker process ID.
- /// Cancellation token.
- /// Worker ready response.
+ ///
public Task StartAsync(
string sessionId,
int workerProcessId,
@@ -65,9 +61,7 @@ internal sealed class FakeRuntimeSession : IWorkerRuntimeSession
});
}
- /// Dispatches a command to the STA thread.
- /// The command to dispatch.
- /// The command reply.
+ ///
public Task DispatchAsync(StaCommand command)
{
return Task.Run(
@@ -112,8 +106,7 @@ internal sealed class FakeRuntimeSession : IWorkerRuntimeSession
});
}
- /// Captures current heartbeat snapshot.
- /// Current runtime heartbeat snapshot.
+ ///
public WorkerRuntimeHeartbeatSnapshot CaptureHeartbeat()
{
lock (gate)
@@ -132,9 +125,7 @@ internal sealed class FakeRuntimeSession : IWorkerRuntimeSession
///
public uint? SuppressDrainForBatchSize { get; set; }
- /// Drains queued events up to the specified limit.
- /// Maximum events to drain; 0 drains all.
- /// The drained events.
+ ///
public IReadOnlyList DrainEvents(uint maxEvents)
{
if (SuppressDrainForBatchSize is uint suppressed && maxEvents == suppressed)
@@ -157,8 +148,7 @@ internal sealed class FakeRuntimeSession : IWorkerRuntimeSession
}
}
- /// Drains a pending fault if any.
- /// Pending fault or null.
+ ///
public WorkerFault? DrainFault()
{
return null;
@@ -168,7 +158,7 @@ internal sealed class FakeRuntimeSession : IWorkerRuntimeSession
/// Gets a snapshot of every correlation id passed to
/// . Recording lets the IPC tests
/// assert that a WorkerCancel envelope dispatched on the
- /// gateway side reaches the runtime session — see Worker.Tests-017.
+ /// gateway side reaches the runtime session.
///
public IReadOnlyList CancelledCorrelationIds
{
@@ -189,7 +179,7 @@ internal sealed class FakeRuntimeSession : IWorkerRuntimeSession
/// command), matching the previous test-double behaviour. Mutated
/// and read under lock(gate) to match the locking convention
/// the rest of this fake uses for cancelledCorrelationIds,
- /// snapshot, and events (Worker.Tests-027).
+ /// snapshot, and events.
///
public bool CancelCommandReturnValue
{
@@ -210,9 +200,7 @@ internal sealed class FakeRuntimeSession : IWorkerRuntimeSession
}
}
- /// Cancels command by correlation ID.
- /// The command correlation ID.
- /// True if cancelled; false otherwise.
+ ///
public bool CancelCommand(string correlationId)
{
lock (gate)
@@ -222,16 +210,13 @@ internal sealed class FakeRuntimeSession : IWorkerRuntimeSession
}
}
- /// Requests graceful shutdown.
+ ///
public void RequestShutdown()
{
releaseDispatch.Set();
}
- /// Shuts down gracefully within the specified timeout.
- /// Shutdown timeout period.
- /// Cancellation token.
- /// Shutdown result.
+ ///
public Task ShutdownGracefullyAsync(
TimeSpan timeout,
CancellationToken cancellationToken = default)
@@ -272,7 +257,7 @@ internal sealed class FakeRuntimeSession : IWorkerRuntimeSession
}
}
- /// Disposes resources.
+ ///
public void Dispose()
{
Disposed = true;
diff --git a/src/ZB.MOM.WW.MxGateway.Worker.Tests/TestSupport/LiveMxAccessFactAttribute.cs b/src/ZB.MOM.WW.MxGateway.Worker.Tests/TestSupport/LiveMxAccessFactAttribute.cs
index 9da8fdf..dcd396c 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker.Tests/TestSupport/LiveMxAccessFactAttribute.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker.Tests/TestSupport/LiveMxAccessFactAttribute.cs
@@ -12,7 +12,7 @@ namespace ZB.MOM.WW.MxGateway.Worker.Tests.TestSupport;
/// otherwise report as Passed). Mirrors
/// ZB.MOM.WW.MxGateway.IntegrationTests.LiveMxAccessFactAttribute; both
/// copies bind to the same GatewayContractInfo constant so the
-/// env-var name has a single literal source of truth (Worker.Tests-025).
+/// env-var name has a single literal source of truth.
///
public sealed class LiveMxAccessFactAttribute : FactAttribute
{
diff --git a/src/ZB.MOM.WW.MxGateway.Worker.Tests/TestSupport/NoopMxAccessServer.cs b/src/ZB.MOM.WW.MxGateway.Worker.Tests/TestSupport/NoopMxAccessServer.cs
index 39c7b17..ad05a42 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker.Tests/TestSupport/NoopMxAccessServer.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker.Tests/TestSupport/NoopMxAccessServer.cs
@@ -9,7 +9,7 @@ namespace ZB.MOM.WW.MxGateway.Worker.Tests.TestSupport;
/// but do not exercise any
/// MXAccess COM call. Replaces the per-file NullMxAccessServer copy
/// that previously lived inside AlarmCommandExecutorTests and was
-/// constructed via reflection — see Worker.Tests-016 for the rationale.
+/// constructed via reflection.
///
internal sealed class NoopMxAccessServer : IMxAccessServer
{
@@ -96,7 +96,7 @@ internal sealed class NoopMxAccessServer : IMxAccessServer
///
///
/// Previously duplicated as a nested class in MxAccessCommandExecutorTests.FakeMxAccessComObject.
-/// Consolidated here per Worker.Tests-034 so a future field change to the real COM struct only
+/// Consolidated here so a future field change to the real COM struct only
/// requires updating one place.
///
internal sealed class FakeMxStatus
diff --git a/src/ZB.MOM.WW.MxGateway.Worker.Tests/TestSupport/WorkerFrameTestHelpers.cs b/src/ZB.MOM.WW.MxGateway.Worker.Tests/TestSupport/WorkerFrameTestHelpers.cs
index 4a78baf..9b56b0f 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker.Tests/TestSupport/WorkerFrameTestHelpers.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker.Tests/TestSupport/WorkerFrameTestHelpers.cs
@@ -12,6 +12,7 @@ internal static class WorkerFrameTestHelpers
{
/// Builds a length-prefixed frame from a protobuf message.
/// Message to serialize into the frame payload.
+ /// The length-prefixed frame bytes.
public static byte[] CreateFrame(IMessage message)
{
return CreateFrame(message.ToByteArray());
@@ -19,6 +20,7 @@ internal static class WorkerFrameTestHelpers
/// Builds a length-prefixed frame from a raw payload.
/// Payload bytes to wrap in a frame.
+ /// The length-prefixed frame bytes.
public static byte[] CreateFrame(byte[] payload)
{
byte[] frame = new byte[sizeof(uint) + payload.Length];
diff --git a/src/ZB.MOM.WW.MxGateway.Worker/Bootstrap/WorkerConsoleLogger.cs b/src/ZB.MOM.WW.MxGateway.Worker/Bootstrap/WorkerConsoleLogger.cs
index 68c98d4..534d72d 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker/Bootstrap/WorkerConsoleLogger.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker/Bootstrap/WorkerConsoleLogger.cs
@@ -16,17 +16,13 @@ public sealed class WorkerConsoleLogger : IWorkerLogger
_writer = writer ?? throw new ArgumentNullException(nameof(writer));
}
- /// Writes an informational log entry.
- /// Name of the event being logged.
- /// Event fields and values to log.
+ ///
public void Information(string eventName, IReadOnlyDictionary fields)
{
Write("Information", eventName, fields);
}
- /// Writes an error log entry.
- /// Name of the event being logged.
- /// Event fields and values to log.
+ ///
public void Error(string eventName, IReadOnlyDictionary fields)
{
Write("Error", eventName, fields);
diff --git a/src/ZB.MOM.WW.MxGateway.Worker/Bootstrap/WorkerLogRedactor.cs b/src/ZB.MOM.WW.MxGateway.Worker/Bootstrap/WorkerLogRedactor.cs
index 25152aa..cfbef58 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker/Bootstrap/WorkerLogRedactor.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker/Bootstrap/WorkerLogRedactor.cs
@@ -28,6 +28,7 @@ public static class WorkerLogRedactor
/// Redacts sensitive field values from a log field dictionary.
///
/// Dictionary of field names and values.
+ /// A new dictionary with sensitive field values replaced by .
public static Dictionary RedactFields(IReadOnlyDictionary fields)
{
Dictionary redactedFields = [];
@@ -45,6 +46,7 @@ public static class WorkerLogRedactor
///
/// Name of the field to check.
/// Value to redact if sensitive.
+ /// The redacted placeholder when looks sensitive; otherwise the original value.
public static object? RedactValue(string fieldName, object? value)
{
if (value is null)
diff --git a/src/ZB.MOM.WW.MxGateway.Worker/Conversion/MxStatusProxyConverter.cs b/src/ZB.MOM.WW.MxGateway.Worker/Conversion/MxStatusProxyConverter.cs
index aad07a4..8deff4d 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker/Conversion/MxStatusProxyConverter.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker/Conversion/MxStatusProxyConverter.cs
@@ -11,6 +11,7 @@ public sealed class MxStatusProxyConverter
{
/// Converts a single status object to a protobuf message, reflecting all fields and diagnostics.
/// COM status object to convert.
+ /// The converted protobuf status message.
public MxStatusProxy Convert(object status)
{
if (status is null)
@@ -38,6 +39,7 @@ public sealed class MxStatusProxyConverter
/// Converts an array of status objects, handling nulls gracefully.
/// Array of COM status objects; null returns empty list.
+ /// The converted protobuf status messages, one per input element (null entries become an "Unknown" placeholder).
public IReadOnlyList ConvertMany(Array? statuses)
{
if (statuses is null)
@@ -67,6 +69,7 @@ public sealed class MxStatusProxyConverter
/// Preserves completion-only status bytes as a diagnostic hex string since they cannot be unpacked.
/// Status bytes to encode as hex string.
+ /// The diagnostic hex-encoded representation of .
public string PreserveCompletionOnlyStatusBytes(byte[] statusBytes)
{
if (statusBytes is null)
diff --git a/src/ZB.MOM.WW.MxGateway.Worker/Conversion/VariantConverter.cs b/src/ZB.MOM.WW.MxGateway.Worker/Conversion/VariantConverter.cs
index 74e2d40..8d7c4a7 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker/Conversion/VariantConverter.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker/Conversion/VariantConverter.cs
@@ -270,7 +270,7 @@ public sealed class VariantConverter
// (a 64-bit 100-ns tick count since 1601). Only a genuine 64-bit source
// (long) can carry a valid full FILETIME; a uint can only hold the low
// 32 bits, which DateTime.FromFileTimeUtc would silently render as a
- // near-1601 timestamp. For uint sources fall through to the integer
+ // timestamp near the 1601 epoch. For uint sources fall through to the integer
// projection rather than producing a bogus timestamp.
if (expectedDataType == MxDataType.Time && value is long)
{
diff --git a/src/ZB.MOM.WW.MxGateway.Worker/Ipc/IWorkerPipeClient.cs b/src/ZB.MOM.WW.MxGateway.Worker/Ipc/IWorkerPipeClient.cs
index 37bad96..c42b21d 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker/Ipc/IWorkerPipeClient.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker/Ipc/IWorkerPipeClient.cs
@@ -10,6 +10,7 @@ public interface IWorkerPipeClient
/// Connects to the gateway and runs the worker until the session ends or is cancelled.
/// Configuration options.
/// Token to cancel the asynchronous operation.
+ /// A task that represents the asynchronous operation.
Task RunAsync(
WorkerOptions options,
CancellationToken cancellationToken = default);
diff --git a/src/ZB.MOM.WW.MxGateway.Worker/Ipc/WorkerFrameReader.cs b/src/ZB.MOM.WW.MxGateway.Worker/Ipc/WorkerFrameReader.cs
index 3ff27a0..8a03900 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker/Ipc/WorkerFrameReader.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker/Ipc/WorkerFrameReader.cs
@@ -27,6 +27,7 @@ public sealed class WorkerFrameReader
/// Reads and validates a single length-prefixed frame from the stream.
/// Token to cancel the asynchronous operation.
+ /// The validated read from the stream.
public async Task ReadAsync(CancellationToken cancellationToken = default)
{
byte[] lengthPrefix = new byte[sizeof(uint)];
diff --git a/src/ZB.MOM.WW.MxGateway.Worker/Ipc/WorkerFrameWriter.cs b/src/ZB.MOM.WW.MxGateway.Worker/Ipc/WorkerFrameWriter.cs
index 478f6eb..a80d548 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker/Ipc/WorkerFrameWriter.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker/Ipc/WorkerFrameWriter.cs
@@ -28,6 +28,7 @@ public sealed class WorkerFrameWriter
/// Writes a worker envelope frame to the stream with length prefix.
/// Worker envelope to write.
/// Token to cancel the asynchronous operation.
+ /// A task that represents the asynchronous operation.
public async Task WriteAsync(
WorkerEnvelope envelope,
CancellationToken cancellationToken = default)
diff --git a/src/ZB.MOM.WW.MxGateway.Worker/Ipc/WorkerPipeClient.cs b/src/ZB.MOM.WW.MxGateway.Worker/Ipc/WorkerPipeClient.cs
index bfba1be..ab47518 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker/Ipc/WorkerPipeClient.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker/Ipc/WorkerPipeClient.cs
@@ -139,11 +139,7 @@ public sealed class WorkerPipeClient : IWorkerPipeClient
_connectAttemptTimeoutMilliseconds = connectAttemptTimeoutMilliseconds;
}
- ///
- /// Runs the worker by connecting to the gateway and executing the frame protocol.
- ///
- /// Worker configuration options.
- /// Token to cancel the asynchronous operation.
+ ///
public async Task RunAsync(
WorkerOptions options,
CancellationToken cancellationToken = default)
diff --git a/src/ZB.MOM.WW.MxGateway.Worker/Ipc/WorkerPipeSession.cs b/src/ZB.MOM.WW.MxGateway.Worker/Ipc/WorkerPipeSession.cs
index de4f199..b13b601 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker/Ipc/WorkerPipeSession.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker/Ipc/WorkerPipeSession.cs
@@ -106,15 +106,9 @@ public sealed class WorkerPipeSession
/// Runs the worker session, completing the handshake and processing messages until cancellation.
/// Token to cancel the asynchronous operation.
+ /// A task that represents the asynchronous operation.
public async Task RunAsync(CancellationToken cancellationToken = default)
{
- // Worker-025: the factory delegate itself is null-checked in the
- // constructor, but its return value is not — a factory that returned
- // null would NRE on the StartAsync lambda below. Throw a diagnostic
- // exception instead so the failure is unambiguous (and so the
- // finally block's _runtimeSession?.Dispose() can't silently no-op
- // on a torn half-initialized session). Mirrors the same pattern
- // AlarmCommandHandler.Subscribe uses for its consumerFactory().
_runtimeSession = _runtimeSessionFactory()
?? throw new InvalidOperationException(
"Worker runtime session factory returned null.");
@@ -142,6 +136,7 @@ public sealed class WorkerPipeSession
/// Completes the gateway startup handshake using default MXAccess initialization.
/// Token to cancel the asynchronous operation.
+ /// A task that represents the asynchronous operation.
public Task CompleteStartupHandshakeAsync(CancellationToken cancellationToken = default)
{
return CompleteStartupHandshakeAsync(InitializeMxAccessAsync, cancellationToken);
@@ -150,6 +145,7 @@ public sealed class WorkerPipeSession
/// Completes the gateway startup handshake with custom MXAccess initialization that returns void.
/// Async function to initialize MXAccess.
/// Token to cancel the asynchronous operation.
+ /// A task that represents the asynchronous operation.
public async Task CompleteStartupHandshakeAsync(
Func initializeMxAccessAsync,
CancellationToken cancellationToken = default)
@@ -171,6 +167,7 @@ public sealed class WorkerPipeSession
/// Completes the gateway startup handshake with custom MXAccess initialization that returns WorkerReady.
/// Async function to initialize MXAccess and return ready state.
/// Token to cancel the asynchronous operation.
+ /// A task that represents the asynchronous operation.
public async Task CompleteStartupHandshakeAsync(
Func> initializeMxAccessAsync,
CancellationToken cancellationToken = default)
@@ -811,7 +808,7 @@ public sealed class WorkerPipeSession
/// command (e.g. ReadBulk waiting timeout_ms for the
/// first OnDataChange callback) freezes LastActivityUtc for the
/// duration of the wait even though the worker is healthy. To avoid
- /// self-faulting a healthy in-flight command (Worker-017), the
+ /// self-faulting a healthy in-flight command, the
/// watchdog is suppressed while CurrentCommandCorrelationId is
/// non-empty — the worker already advertises the in-flight command on
/// each heartbeat, so the gateway has the signal it needs to decide
@@ -819,18 +816,6 @@ public sealed class WorkerPipeSession
/// STA (no command in flight and no activity), which is the only case
/// the watchdog can usefully distinguish from a slow command.
///
- ///
- /// Worker-023: the in-flight-command suppression is itself bounded by
- /// WorkerPipeSessionOptions.HeartbeatStuckCeiling. A truly stuck
- /// synchronous COM call (e.g. against a dead MXAccess provider whose
- /// cross-apartment marshaler is permanently blocked) leaves
- /// CurrentCommandCorrelationId non-empty forever; without an
- /// upper bound the worker-side StaHung watchdog would be
- /// permanently defeated and only the gateway's per-command timeout
- /// would catch the hang. Once LastActivityUtc has been stale
- /// for longer than HeartbeatStuckCeiling the watchdog fires
- /// StaHung regardless of whether a command is in flight.
- ///
private async Task ReportWatchdogFaultIfNeededAsync(
WorkerRuntimeHeartbeatSnapshot snapshot,
CancellationToken cancellationToken)
@@ -852,12 +837,6 @@ public sealed class WorkerPipeSession
// point this branch stops being taken. The heartbeat already
// surfaces the in-flight correlation id so the gateway can apply
// its own per-command timeout if it considers the command too slow.
- //
- // Worker-023: once staleFor exceeds HeartbeatStuckCeiling we fall
- // through to the fault path even with a command in flight — a
- // truly stuck synchronous COM call would otherwise keep
- // CurrentCommandCorrelationId non-empty indefinitely and the
- // worker-side watchdog would never fire.
return;
}
diff --git a/src/ZB.MOM.WW.MxGateway.Worker/Ipc/WorkerPipeSessionOptions.cs b/src/ZB.MOM.WW.MxGateway.Worker/Ipc/WorkerPipeSessionOptions.cs
index 995e445..f8efe8d 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker/Ipc/WorkerPipeSessionOptions.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker/Ipc/WorkerPipeSessionOptions.cs
@@ -35,7 +35,7 @@ public sealed class WorkerPipeSessionOptions
///
/// Gets or sets the defensive upper bound on how long the watchdog
/// will suppress its StaHung fault while a command is in
- /// flight. Worker-017 suppresses the watchdog when the heartbeat
+ /// flight. The watchdog suppresses itself when the heartbeat
/// snapshot's CurrentCommandCorrelationId is non-empty so a
/// legitimately slow command (e.g. ReadBulk against many
/// uncached tags) does not self-fault — but a truly stuck
diff --git a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/AlarmCommandHandler.cs b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/AlarmCommandHandler.cs
index 16a2be3..de60fce 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/AlarmCommandHandler.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/AlarmCommandHandler.cs
@@ -65,7 +65,7 @@ public sealed class AlarmCommandHandler : IAlarmCommandHandler
}
///
- /// Worker-024: production constructor that also injects an
+ /// Production constructor that also injects an
/// STA-affinity guard. is
/// invoked at the entry of every method that touches the underlying
/// (or the wnwrap COM object
diff --git a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/AlarmDispatcher.cs b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/AlarmDispatcher.cs
index feda915..accc2da 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/AlarmDispatcher.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/AlarmDispatcher.cs
@@ -154,6 +154,7 @@ public sealed class AlarmDispatcher : IDisposable
/// protos for the
/// QueryActiveAlarms RPC's ConditionRefresh stream.
///
+ /// The currently active alarm snapshots.
public IReadOnlyList SnapshotActiveAlarms()
{
if (disposed) throw new ObjectDisposedException(nameof(AlarmDispatcher));
diff --git a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/AlarmRecordTransitionMapper.cs b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/AlarmRecordTransitionMapper.cs
index fefa0c9..aed471c 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/AlarmRecordTransitionMapper.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/AlarmRecordTransitionMapper.cs
@@ -24,6 +24,7 @@ public static class AlarmRecordTransitionMapper
/// .
///
/// The state XML string from AVEVA (e.g., UNACK_ALM, ACK_RTN).
+ /// The parsed , or when unrecognized.
public static MxAlarmStateKind ParseStateKind(string? stateXml)
{
if (string.IsNullOrWhiteSpace(stateXml)) return MxAlarmStateKind.Unspecified;
@@ -51,6 +52,7 @@ public static class AlarmRecordTransitionMapper
///
/// The previous alarm state kind.
/// The current alarm state kind.
+ /// The proto that the state change represents.
public static AlarmTransitionKind MapTransition(
MxAlarmStateKind previous,
MxAlarmStateKind current)
@@ -87,6 +89,7 @@ public static class AlarmRecordTransitionMapper
/// The provider name, or null.
/// The group name, or null.
/// The alarm name, or null.
+ /// The composed Provider!Group.AlarmName reference string.
public static string ComposeFullReference(string? providerName, string? groupName, string? alarmName)
{
string provider = providerName ?? string.Empty;
@@ -113,6 +116,7 @@ public static class AlarmRecordTransitionMapper
/// e.g. "13:26:14.709".
/// Offset of the producer's local time vs UTC, in minutes.
/// DST adjustment already applied to local time, in minutes.
+ /// The reassembled UTC timestamp, or when parsing fails.
public static DateTime ParseTransitionTimestampUtc(
string? xmlDate,
string? xmlTime,
@@ -181,7 +185,7 @@ public static class AlarmRecordTransitionMapper
// GMTOFFSET = minutes east of UTC (or behind, depending on convention).
// The wnwrap convention observed: GMTOFFSET=240, DSTADJUST=0 for
- // EDT (UTC-4) — so the field is "minutes from local to UTC". To get
+ // EDT (4 hours behind UTC) — so the field is "minutes from local to UTC". To get
// UTC, ADD the offset.
DateTime utc = localProducerTime.AddMinutes(gmtOffsetMinutes - dstAdjustMinutes);
return DateTime.SpecifyKind(utc, DateTimeKind.Utc);
diff --git a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/FailoverAlarmConsumer.cs b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/FailoverAlarmConsumer.cs
index 9c12ece..89896d2 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/FailoverAlarmConsumer.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/FailoverAlarmConsumer.cs
@@ -84,7 +84,11 @@ public sealed class FailoverAlarmConsumer : IMxAccessAlarmConsumer
this.standby.AlarmTransitionEmitted += OnChildTransition;
}
- ///
+ ///
+ /// Fires once per detected alarm-state transition, forwarded from whichever
+ /// child (primary or standby) is currently active. See "Active-child event
+ /// forwarding" in the type remarks for the forwarding rules.
+ ///
public event EventHandler? AlarmTransitionEmitted;
///
@@ -100,14 +104,6 @@ public sealed class FailoverAlarmConsumer : IMxAccessAlarmConsumer
public AlarmProviderMode Mode => mode;
///
- ///
- /// Arms BOTH children up front so the standby snapshot is warm at the
- /// moment of failover. The standby is always subscribed even if the
- /// primary's Subscribe throws; a standby subscribe failure is
- /// surfaced (rethrown) but does not count toward primary failover. The
- /// primary subscribe runs through the failure-counting wrapper so a
- /// COM failure on subscribe contributes to the failover threshold.
- ///
public void Subscribe(string subscription)
{
if (disposed) throw new ObjectDisposedException(nameof(FailoverAlarmConsumer));
@@ -131,13 +127,6 @@ public sealed class FailoverAlarmConsumer : IMxAccessAlarmConsumer
}
///
- ///
- /// While the primary is active, drives primary.PollOnce through
- /// the failure-counting wrapper. While degraded (standby active),
- /// drives standby.PollOnce and then runs one failback probe per
- /// call via — the worker drives this on a
- /// timer, so one degraded poll equals one probe tick.
- ///
public void PollOnce()
{
if (disposed) throw new ObjectDisposedException(nameof(FailoverAlarmConsumer));
@@ -318,7 +307,7 @@ public sealed class FailoverAlarmConsumer : IMxAccessAlarmConsumer
// snapshot below throws. A handler exception here must never escape the
// switch — escaping would (a) leave `active` flipped with no
// notification and (b) unwind into RunAlarmPollLoopAsync's trailing
- // catch, which permanently stops alarm polling (Worker-026).
+ // catch, which permanently stops alarm polling.
RaiseModeChanged(AlarmProviderMode.Subtag, reason, hresult);
// Warm the standby snapshot for the gateway hand-off. The gateway
@@ -337,7 +326,7 @@ public sealed class FailoverAlarmConsumer : IMxAccessAlarmConsumer
cleanProbes = 0;
// Guarded so a ProviderModeChanged handler exception cannot escape into
- // the STA poll loop and kill alarm delivery (Worker-026).
+ // the STA poll loop and kill alarm delivery.
RaiseModeChanged(AlarmProviderMode.Alarmmgr, reason, hresult);
}
@@ -375,7 +364,7 @@ public sealed class FailoverAlarmConsumer : IMxAccessAlarmConsumer
// AlarmCommandHandler's eventQueue.Enqueue hitting capacity). The
// switch itself has already taken effect; swallow so the failure
// cannot unwind into RunAlarmPollLoopAsync and permanently stop
- // alarm polling (Worker-026). The event-queue overflow it most
+ // alarm polling. The event-queue overflow it most
// likely signals is already surfaced as a fault on the IPC path.
}
}
diff --git a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/IAlarmCommandHandler.cs b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/IAlarmCommandHandler.cs
index 9b359ca..fa97078 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/IAlarmCommandHandler.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/IAlarmCommandHandler.cs
@@ -34,6 +34,7 @@ public interface IAlarmCommandHandler : IDisposable
/// The operator node name.
/// The operator domain name.
/// The operator full name.
+ /// AVEVA's native alarm status code (0 = success).
int Acknowledge(
Guid alarmGuid,
string comment,
@@ -54,6 +55,7 @@ public interface IAlarmCommandHandler : IDisposable
/// The operator node name.
/// The operator domain name.
/// The operator full name.
+ /// AVEVA's native alarm status code (0 = success).
int AcknowledgeByName(
string alarmName,
string providerName,
@@ -69,6 +71,7 @@ public interface IAlarmCommandHandler : IDisposable
/// prefix matched against AlarmFullReference.
///
/// Optional prefix to filter alarms by.
+ /// The currently active alarms matching the filter.
IReadOnlyList QueryActive(string? alarmFilterPrefix);
///
diff --git a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/IMxAccessAlarmConsumer.cs b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/IMxAccessAlarmConsumer.cs
index b3a18cf..af7d1be 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/IMxAccessAlarmConsumer.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/IMxAccessAlarmConsumer.cs
@@ -101,6 +101,7 @@ public interface IMxAccessAlarmConsumer : IDisposable
/// ConditionRefresh path — operator clients call this after reconnect
/// to seed local Part 9 state.
///
+ /// The most recently parsed snapshot of currently active alarms.
IReadOnlyList SnapshotActiveAlarms();
///
diff --git a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/IWorkerRuntimeSession.cs b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/IWorkerRuntimeSession.cs
index 0785e0b..e3b68e8 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/IWorkerRuntimeSession.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/IWorkerRuntimeSession.cs
@@ -34,6 +34,7 @@ public interface IWorkerRuntimeSession : IDisposable
///
/// Captures a heartbeat snapshot of the runtime state.
///
+ /// The current heartbeat snapshot.
WorkerRuntimeHeartbeatSnapshot CaptureHeartbeat();
///
@@ -46,6 +47,7 @@ public interface IWorkerRuntimeSession : IDisposable
///
/// Drains a pending fault from the queue, if any.
///
+ /// The pending fault, or if none is queued.
WorkerFault? DrainFault();
///
diff --git a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/LmxSubtagAlarmSource.cs b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/LmxSubtagAlarmSource.cs
index e334a9a..66bad84 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/LmxSubtagAlarmSource.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/LmxSubtagAlarmSource.cs
@@ -104,15 +104,10 @@ public sealed class LmxSubtagAlarmSource : ISubtagAlarmSource
this.registered = true;
}
- ///
+ /// Raised when an advised subtag reports a new value.
public event EventHandler? ValueChanged;
///
- ///
- /// Idempotent per address: an address already advised is skipped. The
- /// MXAccess COM object is created and the client registered on the first
- /// call. Must be invoked on the worker's STA.
- ///
public void Advise(IReadOnlyCollection itemAddresses)
{
if (itemAddresses is null)
@@ -145,12 +140,6 @@ public sealed class LmxSubtagAlarmSource : ISubtagAlarmSource
}
///
- ///
- /// Adds the item if it has not been added yet — the write may target a
- /// subtag (e.g. the ack-comment) that was never advised — then writes
- /// with MXAccess user id 0 (unsecured). Must be invoked on the worker's
- /// STA.
- ///
public void Write(string itemAddress, object? value)
{
if (itemAddress is null)
diff --git a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/MxAccessCommandExecutor.cs b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/MxAccessCommandExecutor.cs
index 325a9cc..da91f4b 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/MxAccessCommandExecutor.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/MxAccessCommandExecutor.cs
@@ -84,11 +84,7 @@ public sealed class MxAccessCommandExecutor : IStaCommandExecutor
this.pumpStep = pumpStep ?? (static () => { });
}
- ///
- /// Executes an MXAccess command and returns the reply.
- ///
- /// STA command to execute.
- /// Command reply with result or error details.
+ ///
public MxCommandReply Execute(StaCommand command)
{
if (command is null)
diff --git a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/MxAccessEventMapper.cs b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/MxAccessEventMapper.cs
index 5a66f43..c16b984 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/MxAccessEventMapper.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/MxAccessEventMapper.cs
@@ -37,6 +37,7 @@ public sealed class MxAccessEventMapper
/// Item quality code from MXAccess.
/// Item timestamp from MXAccess.
/// Array of MxStatusProxy values from MXAccess.
+ /// The mapped OnDataChange event.
public MxEvent CreateOnDataChange(
string sessionId,
int serverHandle,
@@ -65,6 +66,7 @@ public sealed class MxAccessEventMapper
/// Handle returned by the worker.
/// Handle returned by the worker.
/// Array of MxStatusProxy values from MXAccess.
+ /// The mapped OnWriteComplete event.
public MxEvent CreateOnWriteComplete(
string sessionId,
int serverHandle,
@@ -87,6 +89,7 @@ public sealed class MxAccessEventMapper
/// Handle returned by the worker.
/// Handle returned by the worker.
/// Array of MxStatusProxy values from MXAccess.
+ /// The mapped OperationComplete event.
public MxEvent CreateOperationComplete(
string sessionId,
int serverHandle,
@@ -133,6 +136,7 @@ public sealed class MxAccessEventMapper
/// The alarm provider that sourced this transition. Defaults to
/// for the native path.
///
+ /// The mapped OnAlarmTransition event.
public MxEvent CreateOnAlarmTransition(
string sessionId,
string alarmFullReference,
@@ -194,6 +198,7 @@ public sealed class MxAccessEventMapper
/// Human-readable reason for the switch.
/// The COM HRESULT that triggered a failover, or 0 for a clean failback.
/// The UTC instant the switch occurred.
+ /// The mapped OnAlarmProviderModeChanged event.
public MxEvent CreateOnAlarmProviderModeChanged(
string sessionId,
AlarmProviderMode mode,
@@ -228,6 +233,7 @@ public sealed class MxAccessEventMapper
/// Array of quality values from MXAccess.
/// Array of timestamp values from MXAccess.
/// Array of MxStatusProxy values from MXAccess.
+ /// The mapped OnBufferedDataChange event.
public MxEvent CreateOnBufferedDataChange(
string sessionId,
int serverHandle,
diff --git a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/MxAccessEventQueue.cs b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/MxAccessEventQueue.cs
index ae8393b..51fe92f 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/MxAccessEventQueue.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/MxAccessEventQueue.cs
@@ -148,6 +148,7 @@ public sealed class MxAccessEventQueue
/// Attempts to dequeue the next event without removing it if empty.
///
/// The dequeued event if successful; null if queue is empty.
+ /// if an event was dequeued; if the queue was empty.
public bool TryDequeue(out WorkerEvent? workerEvent)
{
lock (syncRoot)
@@ -167,6 +168,7 @@ public sealed class MxAccessEventQueue
/// Drains up to maxEvents from the queue; if maxEvents is 0, drains all events.
///
/// Maximum number of events to drain; 0 means drain all.
+ /// The drained events, in enqueue order.
public IReadOnlyList Drain(uint maxEvents)
{
lock (syncRoot)
@@ -209,6 +211,7 @@ public sealed class MxAccessEventQueue
///
/// Returns and clears the fault so it is not reported twice.
///
+ /// The recorded , or if none was recorded or it was already drained.
public WorkerFault? DrainFault()
{
lock (syncRoot)
diff --git a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/MxAccessHandleRegistry.cs b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/MxAccessHandleRegistry.cs
index 1e8526f..e761594 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/MxAccessHandleRegistry.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/MxAccessHandleRegistry.cs
@@ -66,6 +66,7 @@ public sealed class MxAccessHandleRegistry
/// Checks if the registry contains the specified server handle.
/// Handle returned by the worker.
+ /// if the server handle is registered; otherwise, .
public bool ContainsServerHandle(int serverHandle)
{
return serverHandles.ContainsKey(serverHandle);
@@ -106,6 +107,7 @@ public sealed class MxAccessHandleRegistry
/// Checks if the registry contains the specified item handle.
/// Handle returned by the worker.
/// Handle returned by the worker.
+ /// if the item handle is registered; otherwise, .
public bool ContainsItemHandle(
int serverHandle,
int itemHandle)
@@ -149,6 +151,7 @@ public sealed class MxAccessHandleRegistry
/// Handle returned by the worker.
/// Handle returned by the worker.
/// Type of advice to check.
+ /// if the advice handle is registered; otherwise, .
public bool ContainsAdviceHandle(
int serverHandle,
int itemHandle,
diff --git a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/MxAccessSession.cs b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/MxAccessSession.cs
index 77940b1..a82e0a6 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/MxAccessSession.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/MxAccessSession.cs
@@ -47,6 +47,7 @@ public sealed class MxAccessSession : IDisposable
/// Creates a WorkerReady message with session metadata.
/// Process ID of the worker.
+ /// The populated message.
public WorkerReady CreateWorkerReady(int workerProcessId)
{
return new WorkerReady
@@ -70,13 +71,14 @@ public sealed class MxAccessSession : IDisposable
/// the production sink wired by — because the
/// new object() stand-in this factory uses for the COM object
/// would silently bypass
- /// during disposal and mask lifetime regressions (Worker.Tests-026).
+ /// during disposal and mask lifetime regressions.
///
/// The server abstraction to drive.
/// The event sink to attach to the session.
/// Optional handle registry; a fresh one is created when null.
/// Optional value cache; a fresh one is created when null.
/// Optional creation thread id; defaults to the current managed thread id.
+ /// A test-only backed by the supplied test doubles.
///
/// Thrown when is the production
/// . Tests must pass a test
@@ -110,6 +112,7 @@ public sealed class MxAccessSession : IDisposable
/// Factory to create the MXAccess COM object.
/// Event sink to attach to the COM object.
/// Identifier of the session.
+ /// The initialized .
public static MxAccessSession Create(
IMxAccessComObjectFactory factory,
IMxAccessEventSink eventSink,
@@ -176,6 +179,7 @@ public sealed class MxAccessSession : IDisposable
/// Registers a client with MXAccess and returns the server handle.
/// Name of the client to register.
+ /// The server handle assigned to the registered client.
public int Register(string clientName)
{
ThrowIfDisposed();
@@ -199,6 +203,7 @@ public sealed class MxAccessSession : IDisposable
/// Adds an item to an MXAccess server and returns the item handle.
/// Handle returned by the worker.
/// Definition or address of the item to add.
+ /// The item handle assigned to the added item.
public int AddItem(
int serverHandle,
string itemDefinition)
@@ -220,6 +225,7 @@ public sealed class MxAccessSession : IDisposable
/// Handle returned by the worker.
/// Definition or address of the item to add.
/// Context string for the item.
+ /// The item handle assigned to the added item.
public int AddItem2(
int serverHandle,
string itemDefinition,
@@ -358,6 +364,7 @@ public sealed class MxAccessSession : IDisposable
/// Handle returned by the worker.
/// Definition or address of the item to add.
/// Context string for the item.
+ /// The item handle assigned to the added item.
public int AddBufferedItem(
int serverHandle,
string itemDefinition,
@@ -463,6 +470,7 @@ public sealed class MxAccessSession : IDisposable
/// Adds multiple items in bulk, returning success/failure results.
/// Handle returned by the worker.
/// Enumerable of item definitions to add.
+ /// The per-item success/failure results, in the order the tags were provided.
public IReadOnlyList AddItemBulk(
int serverHandle,
IEnumerable tagAddresses)
@@ -499,6 +507,7 @@ public sealed class MxAccessSession : IDisposable
/// Advises on multiple items in bulk, returning success/failure results.
/// Handle returned by the worker.
/// Enumerable of item handles to advise on.
+ /// The per-item success/failure results, in the order the handles were provided.
public IReadOnlyList AdviseItemBulk(
int serverHandle,
IEnumerable itemHandles)
@@ -529,6 +538,7 @@ public sealed class MxAccessSession : IDisposable
/// Removes multiple items in bulk, returning success/failure results.
/// Handle returned by the worker.
/// Enumerable of item handles to remove.
+ /// The per-item success/failure results, in the order the handles were provided.
public IReadOnlyList RemoveItemBulk(
int serverHandle,
IEnumerable itemHandles)
@@ -559,6 +569,7 @@ public sealed class MxAccessSession : IDisposable
/// Removes advice subscriptions from multiple items in bulk, returning success/failure results.
/// Handle returned by the worker.
/// Enumerable of item handles to unadvise.
+ /// The per-item success/failure results, in the order the handles were provided.
public IReadOnlyList UnAdviseItemBulk(
int serverHandle,
IEnumerable itemHandles)
@@ -589,6 +600,7 @@ public sealed class MxAccessSession : IDisposable
/// Adds multiple items and subscribes to them in bulk, returning success/failure results.
/// Handle returned by the worker.
/// Enumerable of item definitions to add and subscribe to.
+ /// The per-item success/failure results, in the order the tags were provided.
public IReadOnlyList SubscribeBulk(
int serverHandle,
IEnumerable tagAddresses)
@@ -633,6 +645,7 @@ public sealed class MxAccessSession : IDisposable
/// Unsubscribes from multiple items in bulk, returning success/failure results.
/// Handle returned by the worker.
/// Enumerable of item handles to unsubscribe from.
+ /// The per-item success/failure results, in the order the handles were provided.
public IReadOnlyList UnsubscribeBulk(
int serverHandle,
IEnumerable itemHandles)
@@ -684,6 +697,7 @@ public sealed class MxAccessSession : IDisposable
/// The MXAccess server handle.
/// The write entries to process.
/// Converts protobuf MxValue to COM-compatible variant.
+ /// The per-entry write results, in the order the entries were provided.
public IReadOnlyList WriteBulk(
int serverHandle,
IReadOnlyList entries,
@@ -716,6 +730,7 @@ public sealed class MxAccessSession : IDisposable
/// The MXAccess server handle.
/// The write2 entries to process.
/// Converts protobuf MxValue to COM-compatible variant.
+ /// The per-entry write results, in the order the entries were provided.
public IReadOnlyList Write2Bulk(
int serverHandle,
IReadOnlyList entries,
@@ -753,6 +768,7 @@ public sealed class MxAccessSession : IDisposable
/// The MXAccess server handle.
/// The WriteSecured entries to process.
/// Converts protobuf MxValue to COM-compatible variant.
+ /// The per-entry write results, in the order the entries were provided.
public IReadOnlyList WriteSecuredBulk(
int serverHandle,
IReadOnlyList entries,
@@ -790,6 +806,7 @@ public sealed class MxAccessSession : IDisposable
/// The MXAccess server handle.
/// The WriteSecured2 entries to process.
/// Converts protobuf MxValue to COM-compatible variant.
+ /// The per-entry write results, in the order the entries were provided.
public IReadOnlyList WriteSecured2Bulk(
int serverHandle,
IReadOnlyList entries,
@@ -838,6 +855,7 @@ public sealed class MxAccessSession : IDisposable
/// The tag addresses to read.
/// The timeout per tag.
/// Action invoked on each poll iteration.
+ /// The per-tag read results, in the order the tags were provided.
public IReadOnlyList ReadBulk(
int serverHandle,
IReadOnlyList tagAddresses,
@@ -1079,6 +1097,7 @@ public sealed class MxAccessSession : IDisposable
}
/// Gracefully shuts down the session, cleaning up all handles.
+ /// The shutdown result, including any per-handle cleanup failures.
public MxAccessShutdownResult ShutdownGracefully()
{
if (disposed)
@@ -1096,7 +1115,7 @@ public sealed class MxAccessSession : IDisposable
return new MxAccessShutdownResult(failures);
}
- /// Releases the MXAccess COM object and resources.
+ ///
public void Dispose()
{
if (disposed)
diff --git a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/MxAccessStaSession.cs b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/MxAccessStaSession.cs
index 385ddab..5353bc3 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/MxAccessStaSession.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/MxAccessStaSession.cs
@@ -17,15 +17,6 @@ public sealed class MxAccessStaSession : IWorkerRuntimeSession
private readonly IMxAccessEventSink eventSink;
private readonly MxAccessEventQueue eventQueue;
private readonly StaRuntime staRuntime;
- // Worker-024: the factory takes an Action so MxAccessStaSession can hand
- // the alarm handler its STA-affinity guard (a closure over
- // alarmConsumerThreadId captured at the factory call site). The handler
- // then invokes the guard at the entry of every method that touches the
- // wnwrap consumer, matching the STA-affinity invariant already enforced
- // for the poll path via EnsureOnAlarmConsumerThread.
- // Worker-9: the third arg is the session's IMxAccessComObjectFactory, so
- // the handler can build the subtag-fallback source's own proxy-server COM
- // object on this STA when a subscribe selects the subtag / failover path.
private readonly Func? alarmCommandHandlerFactory;
private StaCommandDispatcher? commandDispatcher;
private MxAccessSession? session;
@@ -171,13 +162,7 @@ public sealed class MxAccessStaSession : IWorkerRuntimeSession
return StartAsync(string.Empty, workerProcessId, cancellationToken);
}
- ///
- /// Starts the MXAccess COM session with a session ID asynchronously.
- ///
- /// Session identifier.
- /// Worker process identifier.
- /// Cancellation token.
- /// Worker ready message.
+ ///
public async Task StartAsync(
string sessionId,
int workerProcessId,
@@ -204,20 +189,6 @@ public sealed class MxAccessStaSession : IWorkerRuntimeSession
// thread id; RunAlarmPollLoopAsync then asserts each
// PollOnce executes on the same thread.
alarmConsumerThreadId = Environment.CurrentManagedThreadId;
- // Worker-024: hand the handler an affinity guard so each
- // of its command-path entries (Subscribe / Acknowledge /
- // AcknowledgeByName / QueryActive / Unsubscribe / PollOnce)
- // asserts the same STA-affinity invariant the poll path
- // already enforced. Without this the command path relied
- // on convention alone; a future refactor that let a
- // command run off-STA would silently deadlock on
- // cross-apartment marshaling against the wnwrap consumer.
- // Worker-9: the factory also receives the session's
- // IMxAccessComObjectFactory so the subtag-fallback source
- // (LmxSubtagAlarmSource) can create its OWN proxy-server COM
- // object on this STA, isolated from the item pipeline's
- // MxAccessSession. The factory call runs on the STA, so the
- // resulting source is bound to the correct apartment.
alarmCommandHandler = alarmCommandHandlerFactory(
eventQueue,
EnsureOnAlarmConsumerThread,
@@ -295,7 +266,7 @@ public sealed class MxAccessStaSession : IWorkerRuntimeSession
// STA runtime shutting down — stop the loop gracefully.
// The dedicated shutdown type lets us distinguish this
// graceful-stop signal from the STA-affinity assertion
- // raised by EnsureOnAlarmConsumerThread (Worker-008),
+ // raised by EnsureOnAlarmConsumerThread,
// which is also an InvalidOperationException but signals
// a programming-error regression — that case falls through
// to the generic Exception arm below and is recorded as a
@@ -378,11 +349,7 @@ public sealed class MxAccessStaSession : IWorkerRuntimeSession
return fault;
}
- ///
- /// Dispatches a command to the STA thread for execution asynchronously.
- ///
- /// The command to dispatch.
- /// Command reply.
+ ///
public Task DispatchAsync(StaCommand command)
{
if (commandDispatcher is null)
@@ -393,10 +360,7 @@ public sealed class MxAccessStaSession : IWorkerRuntimeSession
return commandDispatcher.DispatchAsync(command);
}
- ///
- /// Captures a heartbeat snapshot of the session's runtime state.
- ///
- /// Heartbeat snapshot.
+ ///
public WorkerRuntimeHeartbeatSnapshot CaptureHeartbeat()
{
uint pendingCommandCount = 0;
@@ -416,38 +380,25 @@ public sealed class MxAccessStaSession : IWorkerRuntimeSession
currentCommandCorrelationId);
}
- ///
- /// Requests graceful shutdown of the command dispatcher.
- ///
+ ///
public void RequestShutdown()
{
commandDispatcher?.RequestShutdown();
}
- ///
- /// Drains up to the specified number of events from the queue.
- ///
- /// Maximum events to drain.
- /// Drained events.
+ ///
public IReadOnlyList DrainEvents(uint maxEvents)
{
return eventQueue.Drain(maxEvents);
}
- ///
- /// Drains a fault from the queue if present.
- ///
- /// Drained fault or null.
+ ///
public WorkerFault? DrainFault()
{
return eventQueue.DrainFault();
}
- ///
- /// Cancels a queued command by correlation ID.
- ///
- /// Correlation ID of the command to cancel.
- /// True if cancelled; otherwise false.
+ ///
public bool CancelCommand(string correlationId)
{
return commandDispatcher?.CancelQueuedCommand(correlationId) ?? false;
@@ -507,12 +458,7 @@ public sealed class MxAccessStaSession : IWorkerRuntimeSession
cancellationToken);
}
- ///
- /// Performs graceful shutdown of the MXAccess session within a timeout.
- ///
- /// Maximum time allowed for shutdown.
- /// Cancellation token.
- /// Shutdown result with any cleanup failures.
+ ///
public async Task ShutdownGracefullyAsync(
TimeSpan timeout,
CancellationToken cancellationToken = default)
@@ -620,7 +566,7 @@ public sealed class MxAccessStaSession : IWorkerRuntimeSession
return result;
}
- /// Releases resources and shuts down the session.
+ ///
public void Dispose()
{
if (disposed)
diff --git a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/MxAccessValueCache.cs b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/MxAccessValueCache.cs
index 7bba593..a4ba24d 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/MxAccessValueCache.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/MxAccessValueCache.cs
@@ -60,6 +60,7 @@ public sealed class MxAccessValueCache
/// MXAccess server handle.
/// MXAccess item handle.
/// The cached value if found.
+ /// if a cached value exists for the handle pair; otherwise .
public bool TryGet(
int serverHandle,
int itemHandle,
@@ -102,6 +103,7 @@ public sealed class MxAccessValueCache
/// Action that pumps any pending Windows messages.
/// The cached value if the update was received before the deadline.
/// How long to sleep between pump cycles. Default 5 ms.
+ /// if an update newer than arrived before the deadline; otherwise .
public bool TryWaitForUpdate(
int serverHandle,
int itemHandle,
@@ -137,6 +139,7 @@ public sealed class MxAccessValueCache
/// Returns the current version for a handle pair, or 0 if no entry exists.
/// MXAccess server handle.
/// MXAccess item handle.
+ /// The current cache entry version, or 0 if no entry exists.
public ulong CurrentVersion(
int serverHandle,
int itemHandle)
diff --git a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/SubtagAlarmConsumer.cs b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/SubtagAlarmConsumer.cs
index 87b0c2f..9ef6ab8 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/SubtagAlarmConsumer.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/SubtagAlarmConsumer.cs
@@ -77,18 +77,14 @@ public sealed class SubtagAlarmConsumer : IMxAccessAlarmConsumer
this.valueChangedHandler = OnValueChanged;
}
- ///
- /// Advises every alarm subtag (active / acked / priority, plus the
- /// ack-comment subtag) and begins listening for value changes. The
- /// ack-comment subtag is advised so it is an active MXAccess item by the
- /// time writes it — MXAccess rejects a
- /// write to an added-but-not-advised item with E_INVALIDARG (confirmed
- /// against live MXAccess). Its value changes carry no transition (the
- /// state machine ignores addresses it does not map to active/acked).
- /// The expression is ignored: the subtag
- /// set is fixed by the watch list.
- ///
- /// The subscription expression (unused in subtag mode).
+ ///
+ ///
+ /// The expression is ignored — the subtag
+ /// set is fixed by the watch list. Also advises the ack-comment subtag so
+ /// it is an active MXAccess item by the time
+ /// writes it; MXAccess rejects a write to an added-but-not-advised item
+ /// with E_INVALIDARG.
+ ///
public void Subscribe(string subscription)
{
if (disposed)
@@ -114,17 +110,12 @@ public sealed class SubtagAlarmConsumer : IMxAccessAlarmConsumer
}
}
- ///
- /// Acknowledges an alarm by its synthetic GUID. Resolves the GUID back
- /// to its alarm full reference and delegates to the by-name write path.
- ///
- /// The synthetic alarm GUID.
- /// The acknowledgment comment.
- /// The operator name (unused in subtag mode).
- /// The operator node (unused in subtag mode).
- /// The operator domain (unused in subtag mode).
- /// The operator full name (unused in subtag mode).
- /// 0 on success; non-zero when the GUID or ack-comment subtag is unknown.
+ ///
+ ///
+ /// Resolves the synthetic GUID back to its alarm full reference and
+ /// delegates to the by-name write path; operator-identity arguments are
+ /// not surfaced through the subtag write.
+ ///
public int AcknowledgeByGuid(
Guid alarmGuid,
string ackComment,
@@ -147,21 +138,12 @@ public sealed class SubtagAlarmConsumer : IMxAccessAlarmConsumer
return WriteAckComment(target, ackComment);
}
- ///
- /// Acknowledges an alarm by its (name, provider, group) tuple. In subtag
- /// mode the comment is written to the target's writable ack-comment
- /// subtag; the operator-identity arguments are not surfaced through the
- /// subtag write.
- ///
- /// The alarm name (object-rooted tag name as the dispatcher derives it).
- /// The provider name used to recompose the full reference for lookup.
- /// The group name used to recompose the full reference for lookup.
- /// The acknowledgment comment.
- /// The operator name (unused in subtag mode).
- /// The operator node (unused in subtag mode).
- /// The operator domain (unused in subtag mode).
- /// The operator full name (unused in subtag mode).
- /// 0 on success; non-zero when no target matches or it lacks an ack-comment subtag.
+ ///
+ ///
+ /// In subtag mode the comment is written to the target's writable
+ /// ack-comment subtag; the operator-identity arguments are not
+ /// surfaced through the subtag write.
+ ///
public int AcknowledgeByName(
string alarmName,
string providerName,
@@ -186,12 +168,11 @@ public sealed class SubtagAlarmConsumer : IMxAccessAlarmConsumer
return WriteAckComment(target, ackComment);
}
- ///
- /// Returns the state machine's currently-active alarm snapshot, with
- /// each record stamped and
- /// assigned its synthetic GUID.
- ///
- /// The active alarm snapshot records.
+ ///
+ ///
+ /// Each returned record is stamped
+ /// and assigned its synthetic GUID.
+ ///
public IReadOnlyList SnapshotActiveAlarms()
{
IReadOnlyList records = stateMachine.SnapshotActive();
@@ -203,9 +184,8 @@ public sealed class SubtagAlarmConsumer : IMxAccessAlarmConsumer
return records;
}
- ///
- /// No-op: the subtag path is event-driven and owns no poll cadence.
- ///
+ ///
+ /// No-op: the subtag path is event-driven and owns no poll cadence.
public void PollOnce()
{
// Subtag mode is event-driven; value changes arrive via the source's
diff --git a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/SubtagAlarmStateMachine.cs b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/SubtagAlarmStateMachine.cs
index 3cf7261..145c01f 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/SubtagAlarmStateMachine.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/SubtagAlarmStateMachine.cs
@@ -274,14 +274,19 @@ public sealed class SubtagAlarmStateMachine
private readonly struct SubtagBinding
{
+ /// Pairs a bound subtag address with the alarm state it feeds and the role it plays.
+ /// The alarm state that this subtag's value changes update.
+ /// The role the bound subtag plays (active, acked, or priority).
public SubtagBinding(AlarmState state, SubtagRole role)
{
State = state;
Role = role;
}
+ /// Gets the alarm state that this subtag's value changes update.
public AlarmState State { get; }
+ /// Gets the role the bound subtag plays (active, acked, or priority).
public SubtagRole Role { get; }
}
@@ -291,13 +296,17 @@ public sealed class SubtagAlarmStateMachine
private readonly string _group;
private readonly string _tagName;
+ /// Initializes the tracked state for one alarm target, deriving its provider/group/tag name.
+ /// The configured alarm subtag target this state tracks.
public AlarmState(AlarmSubtagTarget target)
{
(_providerName, _group, _tagName) = DeriveNameParts(target);
}
+ /// Gets or sets whether the alarm is currently active (raised).
public bool Active { get; set; }
+ /// Gets or sets whether the alarm is currently acknowledged.
public bool Acked { get; set; }
///
@@ -307,11 +316,14 @@ public sealed class SubtagAlarmStateMachine
///
public bool AckedDuringEpisode { get; set; }
+ /// Gets or sets the alarm's priority, as last reported by the priority subtag.
public int Priority { get; set; }
+ /// Gets or sets the UTC timestamp at which the alarm was most recently raised.
public DateTime FirstRaiseUtc { get; set; }
/// Derives the current alarm state from the tracked flags, before a transition is applied.
+ /// The alarm state kind (unspecified, unacknowledged, or acknowledged) implied by the tracked flags.
public MxAlarmStateKind DerivedState()
{
if (!Active)
@@ -322,6 +334,10 @@ public sealed class SubtagAlarmStateMachine
return Acked ? MxAlarmStateKind.AckAlm : MxAlarmStateKind.UnackAlm;
}
+ /// Builds a snapshot record for this alarm at the given state kind and timestamp.
+ /// The alarm state kind to record.
+ /// The UTC timestamp of the transition being recorded.
+ /// The snapshot record describing this alarm's provider, name, priority, state, and timestamp.
public MxAlarmSnapshotRecord BuildRecord(MxAlarmStateKind kind, DateTime timestampUtc)
{
return new MxAlarmSnapshotRecord
diff --git a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/WnWrapAlarmConsumer.cs b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/WnWrapAlarmConsumer.cs
index 250cc97..e41f371 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/WnWrapAlarmConsumer.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/WnWrapAlarmConsumer.cs
@@ -86,7 +86,11 @@ public sealed class WnWrapAlarmConsumer : IMxAccessAlarmConsumer
: DefaultMaxAlarmsPerFetch;
}
- ///
+ ///
+ /// Fires once per detected alarm-state transition (raise, acknowledge,
+ /// clear, or new-alarm-already-acked-on-arrival), dispatched from
+ /// on the calling (STA) thread.
+ ///
public event EventHandler? AlarmTransitionEmitted;
///
@@ -112,8 +116,7 @@ public sealed class WnWrapAlarmConsumer : IMxAccessAlarmConsumer
// only path that lets AlarmAckByName succeed afterwards. The
// v2 Initialize/Register/Subscribe methods on the class
// succeed (return 0) but acks against that consumer state
- // return -55. The v1 prefix path is what WIN-911-style code
- // uses against the same wnwrap library.
+ // return -55.
int init = com.IwwAlarmConsumer_InitializeConsumer(DefaultApplicationName);
if (init != 0)
{
@@ -297,15 +300,15 @@ public sealed class WnWrapAlarmConsumer : IMxAccessAlarmConsumer
}
}
- ///
- /// Synchronously poll the wnwrap consumer once and dispatch any
- /// transitions. STA-bound hosts drive polling by calling this from
+ ///
+ ///
+ /// STA-bound hosts drive polling by calling this from
/// the thread that owns the COM object. The consumer deliberately
/// owns no internal timer: a thread-pool timer would call the
/// apartment-threaded COM object off its owning STA and can block
/// indefinitely on cross-apartment marshaling when the STA is not
/// pumping messages.
- ///
+ ///
public void PollOnce()
{
wwAlarmConsumerClass? com;
@@ -350,7 +353,7 @@ public sealed class WnWrapAlarmConsumer : IMxAccessAlarmConsumer
/// after a successful
/// GetXmlCurrentAlarms2 call; exposed as internal static
/// so the diff rules can be unit-tested without driving the
- /// wnwrapConsumer COM object (Worker.Tests-022).
+ /// wnwrapConsumer COM object.
///
///
/// Rules:
@@ -396,6 +399,7 @@ public sealed class WnWrapAlarmConsumer : IMxAccessAlarmConsumer
/// resync).
///
/// The XML snapshot payload.
+ /// The parsed alarm snapshot records, keyed by alarm GUID.
public static Dictionary ParseSnapshotXml(string xml)
{
Dictionary records =
@@ -460,6 +464,7 @@ public sealed class WnWrapAlarmConsumer : IMxAccessAlarmConsumer
///
/// The 32-character hex GUID string.
/// The parsed GUID, or Empty if parsing fails.
+ /// if was successfully parsed; otherwise, .
public static bool TryParseHexGuid(string? hex, out Guid guid)
{
guid = Guid.Empty;
@@ -487,6 +492,7 @@ public sealed class WnWrapAlarmConsumer : IMxAccessAlarmConsumer
/// so the worker doesn't fail to start.
///
/// The subscription expression.
+ /// The XML query payload to pass to SetXmlAlarmQuery.
internal static string ComposeXmlAlarmQuery(string subscription)
{
string node = Environment.MachineName;
diff --git a/src/ZB.MOM.WW.MxGateway.Worker/Sta/StaComApartmentInitializer.cs b/src/ZB.MOM.WW.MxGateway.Worker/Sta/StaComApartmentInitializer.cs
index ca332db..3d40b4b 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker/Sta/StaComApartmentInitializer.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker/Sta/StaComApartmentInitializer.cs
@@ -9,7 +9,7 @@ public sealed class StaComApartmentInitializer : IStaComApartmentInitializer
private const int SOk = 0;
private const int SFalse = 1;
- /// Initializes the COM apartment in single-threaded mode.
+ ///
public void Initialize()
{
int hresult = CoInitializeEx(IntPtr.Zero, CoInitializeApartmentThreaded);
@@ -19,7 +19,7 @@ public sealed class StaComApartmentInitializer : IStaComApartmentInitializer
}
}
- /// Uninitializes the COM apartment.
+ ///
public void Uninitialize()
{
CoUninitialize();
diff --git a/src/ZB.MOM.WW.MxGateway.Worker/Sta/StaMessagePump.cs b/src/ZB.MOM.WW.MxGateway.Worker/Sta/StaMessagePump.cs
index 3284aa3..74d31b7 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker/Sta/StaMessagePump.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker/Sta/StaMessagePump.cs
@@ -43,6 +43,7 @@ public sealed class StaMessagePump
}
/// Pumps and dispatches all pending Windows messages, returning the count processed.
+ /// The number of messages pumped and dispatched.
public int PumpPendingMessages()
{
int pumpedMessages = 0;
diff --git a/src/ZB.MOM.WW.MxGateway.Worker/Sta/StaRuntime.cs b/src/ZB.MOM.WW.MxGateway.Worker/Sta/StaRuntime.cs
index 3f90b7f..2ba2d37 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker/Sta/StaRuntime.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker/Sta/StaRuntime.cs
@@ -86,6 +86,7 @@ public sealed class StaRuntime : IDisposable
/// wait. Callers must already be on the STA; the method is otherwise
/// safe (PeekMessage simply finds no messages).
///
+ /// The number of messages pumped.
public int PumpPendingMessages() => messagePump.PumpPendingMessages();
///
@@ -212,9 +213,7 @@ public sealed class StaRuntime : IDisposable
return stopped;
}
- ///
- /// Releases resources used by the STA runtime.
- ///
+ ///
public void Dispose()
{
if (disposed)
diff --git a/src/ZB.MOM.WW.MxGateway.Worker/Sta/StaWorkItem.cs b/src/ZB.MOM.WW.MxGateway.Worker/Sta/StaWorkItem.cs
index e7d7504..8861236 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker/Sta/StaWorkItem.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker/Sta/StaWorkItem.cs
@@ -41,7 +41,7 @@ internal sealed class StaWorkItem : IStaWorkItem
private TaskCompletionSource Completion { get; }
- /// Cancels the work item before execution begins.
+ ///
public void CancelBeforeExecution()
{
if (Interlocked.CompareExchange(ref started, 1, 0) == 0)
@@ -51,7 +51,7 @@ internal sealed class StaWorkItem : IStaWorkItem
}
}
- /// Executes the work item command.
+ ///
public void Execute()
{
if (Interlocked.CompareExchange(ref started, 1, 0) != 0)
diff --git a/src/ZB.MOM.WW.MxGateway.Worker/WorkerApplication.cs b/src/ZB.MOM.WW.MxGateway.Worker/WorkerApplication.cs
index d253d86..cdf6e48 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker/WorkerApplication.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker/WorkerApplication.cs
@@ -11,6 +11,7 @@ public static class WorkerApplication
{
/// Initializes and runs the worker with default environment and logging.
/// Command-line arguments.
+ /// The process exit code.
public static int Run(string[] args)
{
return Run(
@@ -23,6 +24,7 @@ public static class WorkerApplication
/// Command-line arguments.
/// Worker environment for resolving configuration.
/// Worker logger for diagnostics.
+ /// The process exit code.
public static int Run(
string[] args,
IWorkerEnvironment environment,
@@ -40,6 +42,7 @@ public static class WorkerApplication
/// Worker environment for resolving configuration.
/// Worker logger for diagnostics.
/// Named pipe client for gateway communication.
+ /// The process exit code.
public static int Run(
string[] args,
IWorkerEnvironment environment,