diff --git a/.github/workflows/v2-ci.yml b/.github/workflows/v2-ci.yml index b9263da5..d2e81dd4 100644 --- a/.github/workflows/v2-ci.yml +++ b/.github/workflows/v2-ci.yml @@ -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: > diff --git a/CLAUDE.md b/CLAUDE.md index 1c68f757..60374f76 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Mqtt.Tests/MqttConnectionTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Mqtt.Tests/MqttConnectionTests.cs index 9311df52..16a2e4a3 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Mqtt.Tests/MqttConnectionTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Mqtt.Tests/MqttConnectionTests.cs @@ -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. /// + [Trait("Category", "SlowUnit")] [Fact] public async Task ConnectAsync_BlackholeBroker_FailsWithinDeadline_NoHang() { @@ -95,6 +96,7 @@ public sealed class MqttConnectionTests /// IMqttClient held across failures. A failed attempt must therefore leave the /// connection usable rather than poisoned. /// + [Trait("Category", "SlowUnit")] [Fact] public async Task ConnectAsync_CanBeRetriedOnTheSameInstanceAfterAFailedAttempt() { @@ -592,6 +594,7 @@ public sealed class MqttConnectionTests /// InitializeAsync) — it must not silently leave a background reconnect worker /// hammering an endpoint the caller has already given up on. /// + [Trait("Category", "SlowUnit")] [Fact] public async Task ConnectAsync_InitialAttemptFails_LeavesNoBackgroundReconnectStorm() { @@ -729,6 +732,7 @@ public sealed class MqttConnectionTests /// Faulted) — and the backoff keeps the retry rate low enough that a handful of /// attempts, not hundreds, land in the observation window. /// + [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. /// + [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 attempt so the /// NEXT genuine outage waits max backoff instead of min. /// + [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. /// + [Trait("Category", "SlowUnit")] [Fact] public async Task AfterACallerWinsTheRace_TheNextGenuineOutageStillRecoversAtMinBackoff() { @@ -971,6 +978,7 @@ public sealed class MqttConnectionTests /// dispose-elapsed assertion cannot tell those apart — DisposeAsync's own bounded join /// returns promptly either way, leaving the supervisor alive behind it. /// + [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. /// + [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. /// + [Trait("Category", "SlowUnit")] [Fact] public async Task Reconnect_BrokerStartsRefusing_FaultsAndStopsTheSupervisor() { @@ -1179,6 +1189,7 @@ public sealed class MqttConnectionTests } /// The mirror case: a transient refusal keeps the supervisor retrying under backoff. + [Trait("Category", "SlowUnit")] [Fact] public async Task Reconnect_TransientRefusal_KeepsRetryingUnderBackoff() { diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Mqtt.Tests/MqttDriverDiscoveryTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Mqtt.Tests/MqttDriverDiscoveryTests.cs index ba49a6c1..f57d0c3b 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Mqtt.Tests/MqttDriverDiscoveryTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Mqtt.Tests/MqttDriverDiscoveryTests.cs @@ -211,6 +211,7 @@ public sealed class MqttDriverDiscoveryTests /// fails must land rather than letting the failure escape /// with the health surface still claiming Healthy. /// + [Trait("Category", "SlowUnit")] [Fact] public async Task ReinitializeAsync_SessionChangingDelta_Rebuilds_AndFaultsOnUnreachableBroker() { diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Mqtt.Tests/MqttDriverSessionLifecycleTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Mqtt.Tests/MqttDriverSessionLifecycleTests.cs index 38bdeaa0..5622a682 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Mqtt.Tests/MqttDriverSessionLifecycleTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Mqtt.Tests/MqttDriverSessionLifecycleTests.cs @@ -83,6 +83,7 @@ public sealed class MqttDriverSessionLifecycleTests /// The control for the test above: the normalization must not quietly rewrite a legal /// value, or a deployment's authored connect budget would silently become the default. /// + [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. /// /// + [Trait("Category", "SlowUnit")] [Fact] public async Task ABrokerDrop_MarksCachedRefsBadNoCommunication_AndTheIdenticalReplayRecovers() { diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Mqtt.Tests/SparkplugPrimaryHostTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Mqtt.Tests/SparkplugPrimaryHostTests.cs index cfdcdd5a..48657c28 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Mqtt.Tests/SparkplugPrimaryHostTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Mqtt.Tests/SparkplugPrimaryHostTests.cs @@ -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. /// + [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. /// + [Trait("Category", "SlowUnit")] [Fact] public async Task Disposing_while_a_reconcile_is_in_flight_does_not_fault_on_a_disposed_gate() {