fix(opcuaclient): wire ProbeTimeoutSeconds into the Test Connect probe (R06:O-4)

The knob was authored by the AdminUI and parsed, but consumed by nothing. It now
caps the probe deadline (shorter of the caller's budget and the config; never
extends it; 1 s floor), the same rule as the Galaxy probe.

Refs #743

Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
This commit is contained in:
Joseph Doherty
2026-10-22 15:28:20 -04:00
parent cba3b2e960
commit d98f4a5bf1
3 changed files with 75 additions and 1 deletions
@@ -51,7 +51,8 @@ public sealed class OpcUaClientDriverProbe : IDriverProbe
// Bound the whole probe (both phases) by the caller timeout, independent of `ct` — a
// stalled OPC UA endpoint that accepts TCP but never answers GetEndpoints must not block
// the probe when the caller passes CancellationToken.None.
// the probe when the caller passes CancellationToken.None. The config's ProbeTimeoutSeconds caps it (O-4).
timeout = EffectiveTimeout(timeout, opts.ProbeTimeoutSeconds);
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
cts.CancelAfter(timeout);
@@ -106,6 +107,23 @@ public sealed class OpcUaClientDriverProbe : IDriverProbe
}
}
/// <summary>
/// R06:O-4 — the deadline the probe actually runs under: the caller's budget, tightened by the config's
/// <see cref="OpcUaClientDriverOptions.ProbeTimeoutSeconds"/> when that is positive and shorter (it can only
/// shorten the budget — the caller's own deadline fires regardless). A non-positive caller budget is floored
/// to 1 s so the probe runs briefly rather than cancelling before it starts. Same rule as the Galaxy probe.
/// </summary>
/// <param name="callerTimeout">The budget the host handed the probe.</param>
/// <param name="configuredSeconds">The config's <c>ProbeTimeoutSeconds</c>; ignored when not positive.</param>
/// <returns>The effective probe deadline.</returns>
internal static TimeSpan EffectiveTimeout(TimeSpan callerTimeout, int configuredSeconds)
{
var caller = callerTimeout > TimeSpan.Zero ? callerTimeout : TimeSpan.FromSeconds(1);
if (configuredSeconds <= 0) return caller;
var configured = TimeSpan.FromSeconds(configuredSeconds);
return configured < caller ? configured : caller;
}
/// <summary>
/// Builds a minimal <see cref="ApplicationConfiguration"/> suitable for unauthenticated
/// discovery (GetEndpoints). No PKI stores, no session certificate — discovery traffic