Worker.Tests-008: moved the misplaced WorkerLogRedactor test out of VariantConverterTests into Bootstrap/WorkerLogRedactorTests. Worker.Tests-009: renamed 46 snake_case alarm-test methods to PascalCase Method_Scenario_Expectation. Worker.Tests-010: replaced a weak Assert.Contains with an exact assertion against the real diagnostic message and corrected the XML doc. Worker.Tests-011: renamed and re-documented a cancellation test that overstated what it proved. Worker.Tests-012: added an oversized-frame (MessageTooLarge) test; renamed the mislabeled zero-length-payload test. Worker.Tests-013: removed the fixed-100ms ThrowIfCompletedAsync helper; the caller now races runTask deterministically. Worker.Tests-014: consolidated duplicated test fakes/helpers (FakeRuntimeSession, NoopComApartmentInitializer, NoopEventSink, frame helpers) into a shared TestSupport namespace. Worker.Tests-015: added MxAccessEventQueue coverage for drain-all (maxEvents 0), empty-queue drain, and enqueue-after-fault. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
27 KiB
Code Review — Worker.Tests
| Field | Value |
|---|---|
| Module | src/MxGateway.Worker.Tests |
| Reviewer | Claude Code |
| Review date | 2026-05-18 |
| Commit reviewed | 6c64030 |
| Status | Reviewed |
| Open findings | 0 |
Checklist coverage
| # | Category | Result |
|---|---|---|
| 1 | Correctness & logic bugs | Issues found: Worker.Tests-010 (weak substring assertion), Worker.Tests-011 (test name overstates what it proves). |
| 2 | mxaccessgw conventions | Tests respect STA-affinity and the WorkerEnvelope frame protocol; naming-convention drift only (Worker.Tests-009). |
| 3 | Concurrency & thread safety | Issues found: Worker.Tests-003/004/013 (wall-clock and fixed-delay timing assertions). |
| 4 | Error handling & resilience | COMException/HResult, pipe-never-appears, malformed frames, shutdown-during-command, watchdog all covered; queue branch gap (Worker.Tests-015). |
| 5 | Security | No real secrets; redaction explicitly tested. No issues found. |
| 6 | Performance & resource management | Issues found: Worker.Tests-005 (MemoryStream not disposed), Worker.Tests-006 (MxAccessStaSession leak on assertion failure). |
| 7 | Design-document adherence | Tests match docs/Worker*.md; docs/WorkerFrameProtocol.md is stale (Worker.Tests-007). |
| 8 | Code organization & conventions | Issues found: Worker.Tests-009 (two naming conventions), Worker.Tests-014 (duplicated test doubles). |
| 9 | Testing coverage | Issues found: Worker.Tests-001 (StaMessagePump untested), Worker.Tests-002 (COM-event delivery untested), Worker.Tests-012 (frame-validation gaps). |
| 10 | Documentation & comments | Issues found: Worker.Tests-008 (misplaced redaction test), Worker.Tests-011 (misleading test name). |
Findings
Worker.Tests-001
| Field | Value |
|---|---|
| Severity | High |
| Category | Testing coverage |
| Location | src/MxGateway.Worker.Tests/Sta/ (no StaMessagePumpTests.cs) |
| Status | Resolved |
Description: StaMessagePump — whose entire reason for existing is pumping Windows messages so MXAccess COM event sink calls deliver onto the STA — has no direct unit test. WaitForWorkOrMessages (timeout conversion, the MsgWaitForMultipleObjectsEx failure path) and PumpPendingMessages (drain count) are exercised only indirectly via StaRuntime, which never asserts the pump returns/throws correctly. The MsgWaitFailed error branch and ToTimeoutMilliseconds edge cases (InfiniteTimeSpan, <= Zero, >= uint.MaxValue) are completely uncovered.
Recommendation: Add StaMessagePumpTests that post a Windows message to the STA thread and assert PumpPendingMessages returns the expected count; cover WaitForWorkOrMessages waking on a signaled event vs timeout; cover ToTimeoutMilliseconds boundaries through an internals-visible seam.
Resolution: 2026-05-18 — Added src/MxGateway.Worker.Tests/Sta/StaMessagePumpTests.cs (8 [Fact] tests, run on dedicated STA threads). Covers WaitForWorkOrMessages null-argument validation, returning immediately when the wake event is pre-signalled, waking when the event is signalled mid-wait, returning on timeout when never signalled, the TimeSpan.Zero (<= Zero) conversion branch, and waking on a WM_NULL Windows message posted to the STA thread (the QS_ALLINPUT path). PumpPendingMessages is covered for both an empty queue (returns 0) and three posted messages (returns 3). Boundary noted in the file: the MsgWaitFailed branch is not exercised because forcing MsgWaitForMultipleObjectsEx to fail needs a deliberately invalid native handle, which is unsafe to construct in-process; ToTimeoutMilliseconds is private static and is covered indirectly through wait-latency assertions rather than reflection.
Worker.Tests-002
| Field | Value |
|---|---|
| Severity | High |
| Category | Testing coverage |
| Location | src/MxGateway.Worker.Tests/MxAccess/MxAccessStaSessionTests.cs, src/MxGateway.Worker.Tests/MxAccess/MxAccessEventMapperTests.cs |
| Status | Resolved |
Description: No test verifies that a COM event raised on the STA thread is converted to protobuf and lands in the MxAccessEventQueue. MxAccessEventMapperTests exercises the mapper directly with hand-built fakes, and AlarmDispatcherTests covers the alarm sink, but the non-alarm COM-event path (MxAccessBaseEventSink/MxAccessComServer event handlers → MxAccessEventMapper → queue, triggered by an actual sink callback) is never end-to-end tested. Given the worker's core purpose is to convert COM events to protobuf, this is a significant gap.
Recommendation: Add a test that invokes the base event sink's data-change handler (via an internal seam or a fake COM event source) and asserts a converted WorkerEvent with correct family/sequence appears in the queue.
Resolution: 2026-05-18 — Added src/MxGateway.Worker.Tests/MxAccess/MxAccessBaseEventSinkTests.cs (5 [Fact] tests). The four MxAccessBaseEventSink COM event handlers (OnDataChange, OnWriteComplete, OperationComplete, OnBufferedDataChange) — the exact delegate targets the MXAccess COM runtime invokes — were widened from private to internal (with XML-doc notes that this is a unit-test seam), and [assembly: InternalsVisibleTo("MxGateway.Worker.Tests")] was added to MxGateway.Worker.csproj. The tests construct a real MxAccessBaseEventSink over a real MxAccessEventMapper and MxAccessEventQueue, invoke each handler with COM-style arguments, and assert a correctly-converted protobuf WorkerEvent (family, body case, server/item handle, value, quality, source timestamp, monotonic WorkerSequence) lands in the queue. Boundary noted in the file: the COM += wire-up in Attach/Detach casts to the sealed LMXProxyServerClass RCW and cannot run without a live MXAccess COM object, so it is not exercised; invoking the handlers directly reproduces an STA-thread COM callback and exercises the genuine conversion + enqueue path.
Worker.Tests-003
| Field | Value |
|---|---|
| Severity | Medium |
| Category | Concurrency & thread safety |
| Location | src/MxGateway.Worker.Tests/Sta/StaRuntimeTests.cs:46-48 |
| Status | Resolved |
Description: InvokeAsync_WakesIdlePumpForQueuedCommand asserts stopwatch.Elapsed < TimeSpan.FromSeconds(2) — a wall-clock assertion that on a loaded CI agent can exceed 2s, producing a false failure. The test also does not actually prove the wake event (vs the 50 ms idle pump) caused the dispatch.
Recommendation: Remove the wall-clock assertion (the awaited result already proves the command ran), or raise the budget substantially with a comment that it is a coarse smoke check.
Resolution: 2026-05-18 — Removed the Stopwatch and the stopwatch.Elapsed < TimeSpan.FromSeconds(2) wall-clock assertion from InvokeAsync_WakesIdlePumpForQueuedCommand. The test already constructs the StaRuntime with a 30-second idle pump period, so the awaited InvokeAsync completing at all proves the command wake event — not the idle pump tick — drove the dispatch; no timing budget is needed. The XML-doc comment now states this explicitly. The now-unused using System.Diagnostics; was removed (TreatWarningsAsErrors).
Worker.Tests-004
| Field | Value |
|---|---|
| Severity | Medium |
| Category | Concurrency & thread safety |
| Location | src/MxGateway.Worker.Tests/MxAccess/MxAccessStaSessionTests.cs:281-329 |
| Status | Resolved |
Description: StartAsync_WithAlarmCommandHandlerFactory_PollOnceCalledViaSta and Dispose_StopsAlarmPollLoop use poll-until loops, and Dispose_StopsAlarmPollLoop additionally does await Task.Delay(1000) then asserts PollCount is unchanged. The 1s "no further polls" window is a timing race: a poll scheduled just before disposal could increment the counter afterward, and a slow agent could simply not run a poll in the window even without correct stop logic.
Recommendation: Make the poll loop deterministically observable — expose a "poll loop stopped" signal or have Dispose join the poll task — then assert on that rather than on elapsed-time silence.
Resolution: 2026-05-18 — MxAccessStaSession.Dispose now joins the alarm poll task (pollTaskToJoin.Wait(TimeSpan.FromSeconds(5))) after cancelling the poll CTS, instead of setting alarmPollTask = null and discarding it. Once Dispose returns, the poll loop has provably exited and no PollOnce call can still be in flight. Dispose_StopsAlarmPollLoop was rewritten to drop the await Task.Delay(1000) "no further polls" window: it now captures PollCount immediately after Dispose() returns and re-asserts equality after a bare await Task.Yield() — a deterministic frozen-count check rather than an elapsed-time race. The success-direction poll-until loop in PollOnceCalledViaSta was left as-is: waiting for an event to occur is sound; only waiting for an event to not occur is the race, and that pattern is now eliminated. Note: ShutdownGracefullyAsync already joined the poll task, so this change makes Dispose consistent with the graceful path.
Worker.Tests-005
| Field | Value |
|---|---|
| Severity | Medium |
| Category | Performance & resource management |
| Location | src/MxGateway.Worker.Tests/Ipc/WorkerFrameProtocolTests.cs:20-31,103-105, src/MxGateway.Worker.Tests/Ipc/WorkerPipeSessionTests.cs:28-31 |
| Status | Resolved |
Description: MemoryStream instances are created and never disposed across the frame-protocol and pipe-session tests (MemoryStream stream = new(); with no using). Disposal is cheap so impact is low, but it is inconsistent with the rest of the suite (which carefully usings CancellationTokenSource, StaRuntime, PipePair). WorkerFrameWriter/WorkerFrameReader are also constructed without disposal.
Recommendation: Wrap MemoryStream (and reader/writer if they are IDisposable) in using declarations for consistency.
Resolution: 2026-05-18 — All six MemoryStream test-body declarations in WorkerFrameProtocolTests.cs and the five inbound/outbound MemoryStream declarations in the WorkerPipeSessionTests.cs handshake tests were converted to using declarations, matching how the rest of the suite handles CancellationTokenSource/StaRuntime/PipePair. Re-triage of the parenthetical: WorkerFrameWriter and WorkerFrameReader are not IDisposable (sealed class with no IDisposable and no Dispose member — verified in src/MxGateway.Worker/Ipc/), so the finding's "reader/writer if they are IDisposable" suggestion does not apply and no change was made there. The shared MemoryStream instances inside the WorkerPipeSessionTests harness/helper classes (ReadWrittenFrames parameter, the PipePair/harness fields) are out of the cited line scope and were left untouched.
Worker.Tests-006
| Field | Value |
|---|---|
| Severity | Medium |
| Category | Performance & resource management |
| Location | src/MxGateway.Worker.Tests/MxAccess/MxAccessStaSessionTests.cs:282,305,315,323 |
| Status | Resolved |
Description: Dispose_StopsAlarmPollLoop constructs MxAccessStaSession session without using (unlike every sibling test) and relies on an explicit session.Dispose(). If an assertion between StartAsync and Dispose() throws, the session — its STA thread and poll loop — leaks for the rest of the run. The StaRuntime is usingd so the thread is eventually reclaimed, but the alarm poll loop and handler are not.
Recommendation: Use using MxAccessStaSession session = ... and drop the manual Dispose(), or wrap the body in try/finally.
Resolution: 2026-05-18 — Dispose_StopsAlarmPollLoop now declares its MxAccessStaSession with a using declaration. The manual session.Dispose() is kept because the test's purpose is to observe poll behaviour across disposal — but MxAccessStaSession.Dispose is idempotent (guarded by the disposed field), so the explicit mid-test call and the using-scope call do not conflict. An assertion thrown anywhere in the body now still tears the session (STA poll loop + alarm handler) down. The cited line numbers in the finding were imprecise — they straddle PollOnceCalledViaSta and Dispose_StopsAlarmPollLoop — but the described root cause (one MxAccessStaSession constructed without using) was singular and is the one in Dispose_StopsAlarmPollLoop; the sibling tests PollOnceCalledViaSta and RunAlarmPollLoop_WhenPollOnceThrows_RecordsFaultOnEventQueue already used using and needed no change.
Worker.Tests-007
| Field | Value |
|---|---|
| Severity | Medium |
| Category | Design-document adherence |
| Location | docs/WorkerFrameProtocol.md:38-49 |
| Status | Resolved |
Description: docs/WorkerFrameProtocol.md instructs running dotnet test src/MxGateway.Tests/MxGateway.Tests.csproj --filter WorkerFrameProtocolTests and states the frame protocol "is part of MxGateway.Server". The frame protocol actually lives in MxGateway.Worker.Ipc and is tested by src/MxGateway.Worker.Tests/Ipc/WorkerFrameProtocolTests.cs. The doc's verification command points at the wrong project and build, so anyone following it after changing the worker frame protocol will not run the relevant tests.
Recommendation: Update docs/WorkerFrameProtocol.md to reference src/MxGateway.Worker.Tests and the x86 worker build (-p:Platform=x86).
Resolution: 2026-05-18 — Rewrote the ## Verification section of docs/WorkerFrameProtocol.md. The test command now targets src/MxGateway.Worker.Tests/MxGateway.Worker.Tests.csproj -p:Platform=x86 --filter WorkerFrameProtocolTests; the build command now targets src/MxGateway.Worker/MxGateway.Worker.csproj -p:Platform=x86. The prose now states the frame protocol lives in MxGateway.Worker.Ipc (naming WorkerFrameReader/WorkerFrameWriter/WorkerFrameProtocolOptions and the WorkerFrameProtocolTests.cs test file) and notes the worker is an x86 process. Verified against the source: the frame-protocol types are confirmed under src/MxGateway.Worker/Ipc/ and the tests under src/MxGateway.Worker.Tests/Ipc/, so the original doc was wrong on both project and component. Fenced code blocks were also relabelled powershell (the build/test commands are run from PowerShell on this Windows dev box).
Worker.Tests-008
| Field | Value |
|---|---|
| Severity | Low |
| Category | Documentation & comments |
| Location | src/MxGateway.Worker.Tests/Conversion/VariantConverterTests.cs:175-182 |
| Status | Resolved |
Description: Redactor_WithCredentialBearingValueFields_RedactsBeforeLogging lives in VariantConverterTests but asserts on WorkerLogRedactor.RedactValue, which has nothing to do with VariantConverter. It is also a near-duplicate of coverage in WorkerLogRedactorTests. Placing redaction coverage inside the variant-converter class is misleading.
Recommendation: Move this test into Bootstrap/WorkerLogRedactorTests.cs (which already exists and tests RedactFields).
Resolution: 2026-05-18 — The misplaced redaction test was removed from VariantConverterTests.cs and re-added to Bootstrap/WorkerLogRedactorTests.cs as RedactValue_WithCredentialBearingFieldNames_ReturnsRedactedValue — alongside the existing RedactFields coverage, where redaction tests belong. Confirmed root cause: the old test asserted only on WorkerLogRedactor.RedactValue and never touched VariantConverter. The now-orphaned using MxGateway.Worker.Bootstrap; was removed from VariantConverterTests.cs (TreatWarningsAsErrors). The new home is RedactValue per-field coverage; WorkerLogRedactorTests.RedactFields_... already covers the dictionary path, so the two are complementary rather than duplicates.
Worker.Tests-009
| Field | Value |
|---|---|
| Severity | Low |
| Category | Code organization & conventions |
| Location | src/MxGateway.Worker.Tests/MxAccess/AlarmCommandHandlerTests.cs, AlarmDispatcherTests.cs, AlarmCommandExecutorTests.cs, AlarmRecordTransitionMapperTests.cs, WnWrapAlarmConsumerXmlTests.cs |
| Status | Resolved |
Description: The alarm-related test files use snake_case method names while the rest of the project uses the Method_State_Result PascalCase convention. docs/style-guides/CSharpStyleGuide.md and the surrounding code establish PascalCase as the project convention; the alarm files diverge.
Recommendation: Rename alarm-test methods to the Method_Scenario_Expectation PascalCase form for one consistent convention.
Resolution: 2026-05-18 — Renamed every [Fact]/[Theory] method in the five alarm test files from snake_case to the project's Method_Scenario_Expectation PascalCase form (46 test methods total: 10 in AlarmCommandHandlerTests, 8 in AlarmDispatcherTests, 12 in AlarmCommandExecutorTests, 8 in AlarmRecordTransitionMapperTests, 9 in WnWrapAlarmConsumerXmlTests minus the existing PascalCase probe methods). Only test methods were renamed — snake_case is not present; the method names that look like helpers (Subscribe, PollOnce, Dispose on the fake doubles) are interface implementations of IAlarmCommandHandler/IAlarmTransitionConsumer/IDisposable and were correctly left unchanged. The suite stays green; xUnit discovers tests by attribute, not name, so the renames are behaviour-neutral.
Worker.Tests-010
| Field | Value |
|---|---|
| Severity | Low |
| Category | Correctness & logic bugs |
| Location | src/MxGateway.Worker.Tests/MxAccess/MxAccessStaSessionTests.cs:230-258 |
| Status | Resolved |
Description: StartAsync_WithoutAlarmCommandHandlerFactory_SubscribeAlarmsReturnsInvalidRequest asserts Assert.Contains("alarm", reply.DiagnosticMessage, StringComparison.OrdinalIgnoreCase). The XML doc claims it verifies the diagnostic says "alarm consumer not configured", but the assertion only checks the substring "alarm" — which would also match an unrelated message like "invalid alarm GUID". The assertion is weaker than the documented intent.
Recommendation: Assert the full diagnostic phrase so the test fails if the diagnostic regresses to a misleading message.
Resolution: 2026-05-18 — The weak Assert.Contains("alarm", ...) was replaced with an exact Assert.Equal against the diagnostic the executor actually emits. Re-triage: the test's XML doc claimed the phrase was "alarm consumer not configured", but MxAccessCommandExecutor.ExecuteSubscribeAlarms (verified in src/MxGateway.Worker/MxAccess/MxAccessCommandExecutor.cs:310-315) produces "SubscribeAlarms requires an alarm command handler; the worker was constructed without one." — the doc was wrong, so both the assertion and the XML doc were corrected to the real phrase. The test now fails if the diagnostic regresses to any other message.
Worker.Tests-011
| Field | Value |
|---|---|
| Severity | Low |
| Category | Documentation & comments |
| Location | src/MxGateway.Worker.Tests/Sta/StaCommandDispatcherTests.cs:92-112 |
| Status | Resolved |
Description: DispatchAsync_WhenCanceledAfterExecutionStarts_StillReturnsLateReply is named and documented as if it proves cancellation arrived after execution began. The test does Started.Wait(...) then cancellation.Cancel(), which proves execution started, but because the executor is already running on the STA the cancellation is inherently a no-op — the test cannot distinguish "cancel was observed and ignored" from "cancel was never checked". The name overstates what is proven.
Recommendation: Either tighten the test (assert the dispatcher's cancel path was reached and declined) or rename/comment it to "cancellation cannot abort an in-flight STA command", matching gateway.md's stated behavior.
Resolution: 2026-05-18 — Took the rename/re-document option. The test is renamed DispatchAsync_WhenCanceledWhileExecuting_DoesNotAbortInFlightCommand and its XML doc rewritten to state exactly what it proves — an in-flight STA command is not aborted by cancellation — and to state explicitly that the test cannot and does not distinguish "cancel observed and ignored" from "cancel never checked". The doc now cites gateway.md's wording ("cannot safely abort an in-flight COM call on the STA"). The test body is unchanged: it already asserts the command runs to completion and returns its normal Ok reply, which is the genuine behaviour. No runtime behaviour changed.
Worker.Tests-012
| Field | Value |
|---|---|
| Severity | Low |
| Category | Testing coverage |
| Location | src/MxGateway.Worker.Tests/Ipc/WorkerFrameProtocolTests.cs |
| Status | Resolved |
Description: docs/WorkerFrameProtocol.md states the reader "rejects zero-length payloads and payloads larger than the configured maximum (default 16 MiB) before allocating the payload buffer." WorkerFrameProtocolTests covers malformed-length, wrong protocol version, wrong session, and malformed payload, but has no test for the zero-length-payload rejection or the oversized-frame rejection — both explicit security-relevant input-validation paths.
Recommendation: Add tests feeding a frame with payload_length == 0 and one with payload_length above the configured maximum, asserting the corresponding WorkerFrameProtocolErrorCode.
Resolution: 2026-05-18 — Re-triage of the zero-length half: the finding's "no test for the zero-length-payload rejection" is partly inaccurate. The pre-existing ReadAsync_WithMalformedLength_ThrowsMalformedLength fed a four-zero-byte stream — which is exactly a frame declaring payload_length == 0 — so the zero-length path was already covered, just under a misleading name (the length prefix itself is well-formed; only the declared length is zero). That test was renamed ReadAsync_WithZeroLengthPayload_ThrowsMalformedLength with an XML doc explaining the four-zero-byte construction, rather than adding a duplicate. The oversized half was a genuine gap: a new ReadAsync_WithPayloadAboveConfiguredMaximum_ThrowsMessageTooLarge constructs WorkerFrameProtocolOptions with a 64-byte maximum, feeds a length prefix of 65, and asserts WorkerFrameProtocolErrorCode.MessageTooLarge — verified against WorkerFrameReader.ReadAsync, both checks fire before the payload buffer is rented. The small configured maximum keeps the test from allocating a multi-megabyte buffer.
Worker.Tests-013
| Field | Value |
|---|---|
| Severity | Low |
| Category | Concurrency & thread safety |
| Location | src/MxGateway.Worker.Tests/Ipc/WorkerPipeSessionTests.cs:539-546 |
| Status | Resolved |
Description: ThrowIfCompletedAsync does an unconditional await Task.Delay(TimeSpan.FromMilliseconds(100)) then checks task.IsCompleted. This adds a fixed 100 ms to the test and only catches a RunAsync that fails within that arbitrary window; a session that faults after 100 ms slips past undetected.
Recommendation: Replace with a deterministic race: await Task.WhenAny(runTask, <first-expected-frame-read>) and assert the run task did not win.
Resolution: 2026-05-18 — ThrowIfCompletedAsync was deleted (it had a single call site, in RunAsync_SendsHeartbeatPayloadFromRuntimeSnapshot). That test now races runTask against the first-heartbeat ReadUntilAsync with Task.WhenAny; if runTask wins it is awaited to surface the underlying fault and the test fails via Assert.Fail. The fixed 100 ms delay is gone — the check is now deterministic: a RunAsync faulting at any time before the first heartbeat is caught, and a healthy run completes as soon as the heartbeat arrives instead of always paying 100 ms.
Worker.Tests-014
| Field | Value |
|---|---|
| Severity | Low |
| Category | Code organization & conventions |
| Location | src/MxGateway.Worker.Tests/Ipc/WorkerPipeClientTests.cs:194, WorkerPipeSessionTests.cs:622, Sta/StaCommandDispatcherTests.cs:348, MxAccess/MxAccessStaSessionTests.cs:334, MxAccess/MxAccessCommandExecutorTests.cs:1124 |
| Status | Resolved |
Description: FakeRuntimeSession, NoopComApartmentInitializer, NoopEventSink/NullEventSink, and the CreateFrame/WriteUInt32LittleEndian helpers are re-implemented independently in multiple test files. The two FakeRuntimeSession implementations have already diverged (one supports BlockDispatch/event enqueue, one does not), and NoopComApartmentInitializer is defined four times.
Recommendation: Extract shared test doubles (NoopComApartmentInitializer, frame helpers, a single configurable FakeRuntimeSession) into a TestSupport folder/namespace consumed by all test classes.
Resolution: 2026-05-18 — Added a src/MxGateway.Worker.Tests/TestSupport/ folder (namespace MxGateway.Worker.Tests.TestSupport) with four shared doubles: NoopComApartmentInitializer, NoopEventSink, WorkerFrameTestHelpers (CreateFrame/WriteUInt32LittleEndian), and a single configurable FakeRuntimeSession. The consolidated FakeRuntimeSession is the richer of the two divergent copies (it supports BlockDispatch, event enqueue, shutdown-timeout, and throw-after-release); the minimal WorkerPipeClientTests caller simply leaves the options unset. The per-file copies were deleted from WorkerPipeClientTests, WorkerPipeSessionTests, StaCommandDispatcherTests, MxAccessStaSessionTests, MxAccessCommandExecutorTests, and WorkerFrameProtocolTests, and the orphaned NullEventSink in AlarmCommandExecutorTests was replaced with the shared NoopEventSink. Re-triage: the finding says NoopComApartmentInitializer "is defined four times" — it was defined three times (StaCommandDispatcherTests, MxAccessStaSessionTests, MxAccessCommandExecutorTests); the fourth alarm-area IStaComApartmentInitializer implementation is StaRuntimeTests.RecordingComApartmentInitializer, which is a recording double (asserts init/uninit ordering), not a no-op, so it was deliberately left in place rather than folded into the shared no-op. Unused using directives left behind by the removals were stripped (TreatWarningsAsErrors).
Worker.Tests-015
| Field | Value |
|---|---|
| Severity | Low |
| Category | Testing coverage |
| Location | src/MxGateway.Worker.Tests/MxAccess/MxAccessEventQueueTests.cs |
| Status | Resolved |
Description: MxAccessEventQueueTests covers monotonic sequencing, drain, capacity overflow, and first-fault-wins, but does not cover Drain with maxEvents: 0 (drain-all) — a branch FakeRuntimeSession.DrainEvents even special-cases — nor draining an empty queue, nor enqueue after a manual RecordFault. These are minor branches but the overflow/fault interaction is the worker's backpressure contract.
Recommendation: Add a Drain(0) drain-all test and an empty-queue drain test.
Resolution: 2026-05-18 — Added three tests to MxAccessEventQueueTests. Drain_WithZeroMaxEvents_DrainsAllEvents covers the maxEvents == 0 drain-all branch in MxAccessEventQueue.Drain (verified at src/MxGateway.Worker/MxAccess/MxAccessEventQueue.cs:174) — three events enqueued, Drain(0) returns all three in order and empties the queue. Drain_WhenQueueIsEmpty_ReturnsEmptyList covers the drainCount == 0 early-return branch for both Drain(0) and Drain(5) on an empty queue. Enqueue_AfterRecordFault_ThrowsInvalidOperationException covers the backpressure contract gap the finding flagged — after a manual RecordFault, Enqueue throws InvalidOperationException ("outbound event queue is faulted") and the event is not queued.