using System.Diagnostics; using System.Net; using System.Net.Sockets; using Shouldly; using Xunit; namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests; /// /// Review 2026-10-21 G-6: ProbeTimeoutSeconds was authored and compared for equivalence but /// never consumed — the probe ran on the caller's budget alone. It now caps the probe deadline /// (it can shorten the caller's budget, never extend it). /// [Trait("Category", "Unit")] public sealed class GalaxyDriverProbeTimeoutTests { /// The effective deadline is the shorter of the caller's budget and the configured cap. [Theory] [InlineData(10, 30, 10)] // caller shorter → caller wins [InlineData(10, 2, 2)] // config shorter → config caps [InlineData(10, 0, 10)] // unset config → caller [InlineData(10, -1, 10)] // nonsense config → caller [InlineData(0, 5, 1)] // no caller budget → 1 s floor beats a longer config [InlineData(0, 0, 1)] // nothing → 1 s floor public void EffectiveTimeout_is_the_shorter_of_caller_and_config(int callerSeconds, int configSeconds, int expectedSeconds) => GalaxyDriverProbe.EffectiveTimeout(TimeSpan.FromSeconds(callerSeconds), configSeconds) .ShouldBe(TimeSpan.FromSeconds(expectedSeconds)); /// /// Against a port that accepts TCP but never speaks gRPC, a 1 s configured cap returns long /// before the caller's 20 s budget. The gRPC handshake itself does the waiting here, so this /// exercises the real deadline path, not the pure helper. /// [Fact] public async Task Configured_cap_shortens_the_probe_against_a_silent_listener() { var listener = new TcpListener(IPAddress.Loopback, 0); listener.Start(); try { var port = ((IPEndPoint)listener.LocalEndpoint).Port; var config = $$""" { "Gateway": { "Endpoint": "http://127.0.0.1:{{port}}", "ApiKeySecretRef": "dev:k", "UseTls": false }, "MxAccess": { "ClientName": "probe-timeout" }, "Repository": {}, "Reconnect": {}, "ProbeTimeoutSeconds": 1 } """; var sw = Stopwatch.StartNew(); var result = await new GalaxyDriverProbe().ProbeAsync(config, TimeSpan.FromSeconds(20), TestContext.Current.CancellationToken); sw.Stop(); result.Ok.ShouldBeFalse(); sw.Elapsed.ShouldBeLessThan(TimeSpan.FromSeconds(10), $"the 1 s configured cap must bound the probe (message: {result.Message})"); result.Message.ShouldNotBeNull(); (result.Message!.Contains("timed out", StringComparison.OrdinalIgnoreCase) || result.Message.Contains("DeadlineExceeded", StringComparison.OrdinalIgnoreCase) || result.Message.Contains("handshake failed", StringComparison.OrdinalIgnoreCase)) .ShouldBeTrue($"unexpected message: '{result.Message}'"); } finally { listener.Stop(); } } }