Merge: deterministic AbCip transport-switch probe tests (libplctag timeout races the probe cancel under load)

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
Joseph Doherty
2026-10-07 23:51:34 -04:00
@@ -116,6 +116,21 @@ public sealed class AbCipTransportSwitchTests
driver.TagFactory.ShouldBeOfType<LibplctagTagFactory>();
}
/// <summary>
/// Positive control for the libplctag path against a listener that accepts TCP and never speaks CIP.
/// libplctag reports the stall two ways that race on the wall clock: the probe's own cancellation
/// ("Probe timed out after Ns") or libplctag's internal timeout, which has the same duration and
/// surfaces as the <c>ErrorTimeout</c> status. Which fires first depends on scheduler load, so both are
/// the libplctag outcome; the native path never produces either (it fails at once with "Connection lost").
/// </summary>
private static void ShouldBeLibplctagTimeout(string? message)
{
message.ShouldNotBeNull();
(message.Contains("timed out", StringComparison.OrdinalIgnoreCase)
|| message.Contains("ErrorTimeout", StringComparison.Ordinal))
.ShouldBeTrue($"expected the libplctag stall outcome (timed out / ErrorTimeout) but was: {message}");
}
/// <summary>
/// The probe's Phase 1 TCP preflight rejects a refused port before any runtime exists, so the native
/// path is proven against a listener that accepts TCP and hangs up: only a runtime that really opens
@@ -150,7 +165,7 @@ public sealed class AbCipTransportSwitchTests
lib.Message!.ShouldNotContain("Connection lost");
// Positive (#574): the libplctag path ran and reported its own outcome (the bounded probe timeout),
// not merely "something other than the native text".
lib.Message!.ShouldContain("timed out");
ShouldBeLibplctagTimeout(lib.Message);
}
/// <summary>
@@ -188,7 +203,7 @@ public sealed class AbCipTransportSwitchTests
lib.Message!.ShouldNotContain("Connection lost");
// Positive (#574): the libplctag path ran and reported its own outcome (the bounded probe timeout),
// not merely "something other than the native text".
lib.Message!.ShouldContain("timed out");
ShouldBeLibplctagTimeout(lib.Message);
}
/// <summary>The probe deserialises with its own string-enum converter; a bad transport is a config error.</summary>
@@ -268,7 +283,7 @@ public sealed class AbCipTransportSwitchTests
unpinned.Message!.ShouldContain("CIP handshake failed: Connection lost");
pinned.Ok.ShouldBeFalse();
pinned.Message!.ShouldNotContain("Connection lost");
pinned.Message!.ShouldContain("timed out"); // positive: libplctag's own outcome (#574)
ShouldBeLibplctagTimeout(pinned.Message); // positive: libplctag's own outcome (#574)
}
/// <summary>Gitea #572: <c>browsePageDelayMs</c> parses (default 5 ms, 0 allowed, negative clamps to 0).</summary>