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.KillWorkerAsyncsession.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.KillWorkerAsyncsession.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. UnackAlmAckAlm @@ -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,