test(mqtt): tag 16 wall-clock-bound tests Category=SlowUnit; document local opt-out (#606)

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
Joseph Doherty
2026-10-08 17:02:44 -04:00
parent 839f32910d
commit ad108f6a02
6 changed files with 22 additions and 1 deletions
+2 -1
View File
@@ -57,7 +57,8 @@ jobs:
# Self-maintaining — a new *.Tests project is covered automatically, no matrix to widen —
# and matches CLAUDE.md's own `dotnet test ZB.MOM.WW.OtOpcUa.slnx` guidance. Excludes only
# the env-gated tiers: E2E (nightly, needs the docker-dev fleet) and LiveIntegration (needs
# a real gateway/PLC/VPN). The skip-gated *.IntegrationTests Assert.Skip when their fixtures
# a real gateway/PLC/VPN). Category=SlowUnit (Mqtt socket-timeout tests, #606) is deliberately NOT
# excluded here — it is a local-dev-loop opt-out only. The skip-gated *.IntegrationTests Assert.Skip when their fixtures
# are unreachable, so they run harmlessly here (and are counted by the skip-gate below).
- name: dotnet test (whole solution, minus E2E + LiveIntegration)
run: >
+4
View File
@@ -359,6 +359,10 @@ dotnet test tests/Core/ZB.MOM.WW.OtOpcUa.Core.Tests # a single tes
dotnet test --filter "FullyQualifiedName~MyTestClass.MyMethod" # a single test
```
`Category=SlowUnit` (#606) tags the 16 wall-clock-bound Driver.Mqtt unit tests (blackhole connect-timeout /
reconnect-backoff windows, ~1 min of the project's 73 s). CI runs them (its filter excludes only E2E +
LiveIntegration); for a fast local loop add `--filter "Category!=SlowUnit"`.
CVE-2025-6965 / GHSA-2m69-gcr7-jv3q (transitive native `SQLitePCLRaw.lib.e_sqlite3`)
is **resolved** — the temporary `NuGetAuditSuppress` was removed once the patched bundle
shipped. `Core.AlarmHistorian` now carries a surgical direct pin of
@@ -30,6 +30,7 @@ public sealed class MqttConnectionTests
/// actually wedges a client — a refused port returns ECONNREFUSED instantly and would pass
/// whether or not any deadline logic existed.
/// </summary>
[Trait("Category", "SlowUnit")]
[Fact]
public async Task ConnectAsync_BlackholeBroker_FailsWithinDeadline_NoHang()
{
@@ -95,6 +96,7 @@ public sealed class MqttConnectionTests
/// <c>IMqttClient</c> held across failures. A failed attempt must therefore leave the
/// connection usable rather than poisoned.
/// </summary>
[Trait("Category", "SlowUnit")]
[Fact]
public async Task ConnectAsync_CanBeRetriedOnTheSameInstanceAfterAFailedAttempt()
{
@@ -592,6 +594,7 @@ public sealed class MqttConnectionTests
/// <c>InitializeAsync</c>) — it must not silently leave a background reconnect worker
/// hammering an endpoint the caller has already given up on.
/// </summary>
[Trait("Category", "SlowUnit")]
[Fact]
public async Task ConnectAsync_InitialAttemptFails_LeavesNoBackgroundReconnectStorm()
{
@@ -729,6 +732,7 @@ public sealed class MqttConnectionTests
/// <c>Faulted</c>) — and the backoff keeps the retry rate low enough that a handful of
/// attempts, not hundreds, land in the observation window.
/// </summary>
[Trait("Category", "SlowUnit")]
[Fact]
public async Task BrokerStaysDown_RetriesUnderBackoff_WithoutHotLooping()
{
@@ -759,6 +763,7 @@ public sealed class MqttConnectionTests
/// Dispose during a reconnect backoff must stop the supervisor dead — no leaked task, no
/// later attempt resurrecting a connection the caller has torn down.
/// </summary>
[Trait("Category", "SlowUnit")]
[Fact]
public async Task Dispose_DuringReconnectBackoff_StopsTheSupervisor_NoFurtherAttempts()
{
@@ -875,6 +880,7 @@ public sealed class MqttConnectionTests
/// against a perfectly healthy connection, at Debug level, inflating <c>attempt</c> so the
/// NEXT genuine outage waits max backoff instead of min.
/// </summary>
[Trait("Category", "SlowUnit")]
[Fact]
public async Task CallerReconnectsWhileTheSupervisorIsInBackoff_SupervisorStandsDownSilently()
{
@@ -917,6 +923,7 @@ public sealed class MqttConnectionTests
/// A stood-down supervisor answers it one min-backoff (2 s) later. The 5 s window separates the
/// two by a clear margin; widening the burn only widens the separation.
/// </remarks>
[Trait("Category", "SlowUnit")]
[Fact]
public async Task AfterACallerWinsTheRace_TheNextGenuineOutageStillRecoversAtMinBackoff()
{
@@ -971,6 +978,7 @@ public sealed class MqttConnectionTests
/// dispose-elapsed assertion cannot tell those apart — <c>DisposeAsync</c>'s own bounded join
/// returns promptly either way, leaving the supervisor alive behind it.
/// </remarks>
[Trait("Category", "SlowUnit")]
[Fact]
public async Task ReSubscribeThatHangsIgnoringItsToken_DoesNotParkTheSupervisor()
{
@@ -1085,6 +1093,7 @@ public sealed class MqttConnectionTests
/// An unrecoverable rejection must not leave a supervisor grinding away every backoff period
/// against a broker that will never accept these credentials.
/// </summary>
[Trait("Category", "SlowUnit")]
[Fact]
public async Task ConnectAsync_RefusedCredentials_StartsNoReconnectSupervisor()
{
@@ -1156,6 +1165,7 @@ public sealed class MqttConnectionTests
/// The supervisor's own attempts go through the same CONNACK check. A broker that starts
/// refusing mid-life (credentials revoked, client banned) must stop the loop, not feed it.
/// </summary>
[Trait("Category", "SlowUnit")]
[Fact]
public async Task Reconnect_BrokerStartsRefusing_FaultsAndStopsTheSupervisor()
{
@@ -1179,6 +1189,7 @@ public sealed class MqttConnectionTests
}
/// <summary>The mirror case: a transient refusal keeps the supervisor retrying under backoff.</summary>
[Trait("Category", "SlowUnit")]
[Fact]
public async Task Reconnect_TransientRefusal_KeepsRetryingUnderBackoff()
{
@@ -211,6 +211,7 @@ public sealed class MqttDriverDiscoveryTests
/// fails must land <see cref="DriverState.Faulted"/> rather than letting the failure escape
/// with the health surface still claiming Healthy.
/// </summary>
[Trait("Category", "SlowUnit")]
[Fact]
public async Task ReinitializeAsync_SessionChangingDelta_Rebuilds_AndFaultsOnUnreachableBroker()
{
@@ -83,6 +83,7 @@ public sealed class MqttDriverSessionLifecycleTests
/// The control for the test above: the normalization must not quietly rewrite a <i>legal</i>
/// value, or a deployment's authored connect budget would silently become the default.
/// </summary>
[Trait("Category", "SlowUnit")]
[Fact]
public async Task InitializeAsync_WithAnAuthoredConnectTimeout_KeepsIt()
{
@@ -126,6 +127,7 @@ public sealed class MqttDriverSessionLifecycleTests
/// can drop a session (which is what this test needs from it) but cannot PUBLISH.
/// </para>
/// </remarks>
[Trait("Category", "SlowUnit")]
[Fact]
public async Task ABrokerDrop_MarksCachedRefsBadNoCommunication_AndTheIdenticalReplayRecovers()
{
@@ -243,6 +243,7 @@ public sealed class SparkplugPrimaryHostTests
/// one-second role sample, forever. It now backs off. The clock is frozen, so the real one-second
/// loop ticks several times without the backoff window ever opening; advancing the clock opens it.
/// </summary>
[Trait("Category", "SlowUnit")]
[Fact]
public async Task A_failing_claim_backs_off_instead_of_redialling_every_tick()
{
@@ -313,6 +314,7 @@ public sealed class SparkplugPrimaryHostTests
/// Deterministic rather than timing-dependent: the gate is taken before the connect starts, so
/// the broker having accepted the socket proves the claim is inside it when dispose runs.
/// </remarks>
[Trait("Category", "SlowUnit")]
[Fact]
public async Task Disposing_while_a_reconcile_is_in_flight_does_not_fault_on_a_disposed_gate()
{