using System.Threading.Tasks; using ScadaLink.CLI.Commands; namespace ScadaLink.CLI.Tests; /// /// Regression tests for the testable pieces of DebugCommands.StreamDebugAsync: /// CLI-010 (Ctrl+C during connect misreported as a connection failure) and /// CLI-012 (non-deterministic exit code after stream termination). /// public class DebugStreamTests { // --- CLI-010: connect-failure classification -------------------------------------- [Fact] public void ClassifyConnectFailure_OperationCanceled_IsTreatedAsCancellation() { // Ctrl+C while StartAsync is still establishing the connection throws // OperationCanceledException — this is a graceful cancellation, not a failure. var result = DebugStreamHelpers.ClassifyConnectFailure( new OperationCanceledException(), cancellationRequested: true); Assert.True(result.IsCancellation); Assert.Equal(0, result.ExitCode); } [Fact] public void ClassifyConnectFailure_TaskCanceled_WhenCancelRequested_IsCancellation() { var result = DebugStreamHelpers.ClassifyConnectFailure( new TaskCanceledException(), cancellationRequested: true); Assert.True(result.IsCancellation); Assert.Equal(0, result.ExitCode); } [Fact] public void ClassifyConnectFailure_RealException_IsConnectionFailure() { var result = DebugStreamHelpers.ClassifyConnectFailure( new HttpRequestException("connection refused"), cancellationRequested: false); Assert.False(result.IsCancellation); Assert.Equal(1, result.ExitCode); } [Fact] public void ClassifyConnectFailure_CanceledExceptionButNoCancelRequested_IsConnectionFailure() { // A cancellation that did not originate from the user (e.g. a server-side abort) // is still a real connection failure. var result = DebugStreamHelpers.ClassifyConnectFailure( new OperationCanceledException(), cancellationRequested: false); Assert.False(result.IsCancellation); Assert.Equal(1, result.ExitCode); } // --- CLI-012: deterministic exit-code resolution ---------------------------------- [Fact] public async Task ResolveStreamExitCodeAsync_TerminationResultSet_PrefersThatResult() { // OnStreamTerminated set exitTcs to 1 — that must win even on the Ctrl+C path. var tcs = new TaskCompletionSource(); tcs.SetResult(1); var code = await DebugStreamHelpers.ResolveStreamExitCodeAsync(tcs.Task); Assert.Equal(1, code); } [Fact] public async Task ResolveStreamExitCodeAsync_NoResult_ReturnsZero() { // Pure Ctrl+C: exitTcs never completed — graceful shutdown, exit 0. var tcs = new TaskCompletionSource(); var code = await DebugStreamHelpers.ResolveStreamExitCodeAsync(tcs.Task); Assert.Equal(0, code); } [Fact] public async Task ResolveStreamExitCodeAsync_ResultArrivesDuringGrace_IsObserved() { // A stream termination racing with Ctrl+C: the result lands shortly after the // wait was cancelled. The grace period must let it be observed deterministically. var tcs = new TaskCompletionSource(); _ = Task.Run(async () => { await Task.Delay(20); tcs.TrySetResult(1); }); var code = await DebugStreamHelpers.ResolveStreamExitCodeAsync(tcs.Task); Assert.Equal(1, code); } }