namespace ScadaLink.CLI.Commands;
///
/// Pure, testable helpers for the debug stream command. The SignalR-driven
/// body itself cannot be unit-tested without a live hub, so
/// the decision logic — connect-failure classification (CLI-010) and exit-code
/// resolution after stream termination (CLI-012) — is extracted here.
///
internal static class DebugStreamHelpers
{
///
/// The maximum time waits for an in-flight
/// TrySetResult (from OnStreamTerminated/Closed) to land after
/// the wait was cancelled by Ctrl+C, so a termination racing with cancellation is
/// observed deterministically rather than depending on scheduling.
///
internal static readonly TimeSpan ExitGracePeriod = TimeSpan.FromMilliseconds(250);
/// Outcome of classifying an exception thrown while connecting.
internal readonly record struct ConnectFailure(bool IsCancellation, int ExitCode);
///
/// Classifies an exception thrown by HubConnection.StartAsync. A
/// cancellation exception that coincides with a user-requested cancellation
/// (Ctrl+C during connect) is a graceful shutdown — exit 0, no error printed.
/// Anything else is a genuine connection failure — exit 1.
///
internal static ConnectFailure ClassifyConnectFailure(Exception ex, bool cancellationRequested)
{
if (cancellationRequested && ex is OperationCanceledException)
return new ConnectFailure(IsCancellation: true, ExitCode: 0);
return new ConnectFailure(IsCancellation: false, ExitCode: 1);
}
///
/// Resolves the debug stream exit code from a single authoritative source —
/// the exitTcs task. If a result was set by OnStreamTerminated or the
/// Closed handler it is always preferred (even when Ctrl+C also fired);
/// a brief grace period covers a termination that races with cancellation. If no
/// result is ever produced (pure Ctrl+C), the stream ended gracefully — exit 0.
///
internal static async Task ResolveStreamExitCodeAsync(Task exitTask)
{
if (exitTask.IsCompletedSuccessfully)
return exitTask.Result;
var completed = await Task.WhenAny(exitTask, Task.Delay(ExitGracePeriod));
if (ReferenceEquals(completed, exitTask) && exitTask.IsCompletedSuccessfully)
return exitTask.Result;
return 0;
}
}