@@ -23,13 +23,15 @@ public class CodexExecTests
2323 private const string DuplexPressureTemplatePlaceholder = "{0}" ;
2424 private const string PosixFixtureSkipReason = "The public CLI yield-boundary fixture currently uses a POSIX executable script." ;
2525 private const string LinuxFixtureSkipReason = "The detached stderr-retention fixture requires Linux setsid." ;
26- private const string StderrClosureFailure = "stderr stream closed " ;
26+ private const string StderrClosureFailure = "Codex CLI process and output cleanup could not be confirmed. " ;
2727 private const string ProcessOutputLimitMessage = "Codex CLI process exceeded the configured output limit." ;
2828 private const string PosixSingleLineOverflowCommand = "printf '%100s\\ n' x; exec /bin/sleep 30" ;
2929 private const string PosixMultiLineOverflowCommand = "printf '1234567890\\ n1234567890\\ n1234567890\\ n'" ;
3030 private const string PosixStandardErrorPressureCommand = "printf '%100s' x >&2; exec /bin/sleep 30" ;
3131 private const string PosixNormalMultiLineCommand = "printf 'first\\ nsecond\\ n'" ;
3232 private const string PosixNonZeroExitBeforeInputCommand = "printf 'provider failed\\ n' >&2; /bin/sleep 0.1; exit 23" ;
33+ private const string PosixZeroExitAfterClosingInputCommand = "exec 0<&-; /bin/sleep 0.1; exit 0" ;
34+ private const string PosixStaysRunningAfterClosingInputCommand = "printf 'started\\ n'; exec 0<&-; exec /bin/sleep 30" ;
3335 private const string WindowsPowerShellPath = "powershell.exe" ;
3436 private const string WindowsNoProfileFlag = "-NoProfile" ;
3537 private const string WindowsNonInteractiveFlag = "-NonInteractive" ;
@@ -39,6 +41,8 @@ public class CodexExecTests
3941 private const string WindowsStandardErrorPressureCommand = "[Console]::Error.Write('x' * 100); Start-Sleep -Seconds 30" ;
4042 private const string WindowsNormalMultiLineCommand = "Write-Output 'first'; Write-Output 'second'" ;
4143 private const string WindowsNonZeroExitBeforeInputCommand = "[Console]::Error.WriteLine('provider failed'); Start-Sleep -Milliseconds 100; exit 23" ;
44+ private const string WindowsZeroExitAfterClosingInputCommand = "[Console]::OpenStandardInput().Dispose(); Start-Sleep -Milliseconds 100; exit 0" ;
45+ private const string WindowsStaysRunningAfterClosingInputCommand = "Write-Output 'started'; [Console]::OpenStandardInput().Dispose(); Start-Sleep -Seconds 30" ;
4246 private const string ExpectedFirstLine = "first" ;
4347 private const string ExpectedSecondLine = "second" ;
4448 private const int SmallOutputLimitCharacters = 64 ;
@@ -808,6 +812,51 @@ public async Task DefaultProcessRunner_NonZeroExitWithCompletedBrokenPipeKeepsCo
808812 }
809813 }
810814
815+ [ Test ]
816+ public async Task DefaultProcessRunner_ZeroExitAfterClosingInputRemainsAnInputFailure ( )
817+ {
818+ var prompt = new string ( PromptCharacter , DuplexPromptCharacters ) ;
819+ var invocation = CreateOutputInvocation (
820+ OperatingSystem . IsWindows ( ) ? WindowsZeroExitAfterClosingInputCommand : PosixZeroExitAfterClosingInputCommand ,
821+ TimeSpan . FromSeconds ( 5 ) , DuplexMaximumProcessOutputCharacters ) with
822+ { Input = prompt } ;
823+ var action = async ( ) =>
824+ {
825+ await foreach ( var _ in new DefaultCodexProcessRunner ( ) . RunAsync ( invocation , NullLogger . Instance , CancellationToken . None ) )
826+ {
827+ }
828+ } ;
829+
830+ var exception = await Assert . That ( action ) . ThrowsException ( ) ;
831+
832+ await Assert . That ( exception ) . IsTypeOf < IOException > ( ) ;
833+ await Assert . That ( exception ) . IsNotTypeOf < CliExecutionFailureException > ( ) ;
834+ }
835+
836+ [ Test ]
837+ public async Task DefaultProcessRunner_StillRunningAfterClosingInputIsBoundedAndUnconfirmed ( )
838+ {
839+ var prompt = new string ( PromptCharacter , DuplexPromptCharacters ) ;
840+ var invocation = CreateOutputInvocation (
841+ OperatingSystem . IsWindows ( ) ? WindowsStaysRunningAfterClosingInputCommand : PosixStaysRunningAfterClosingInputCommand ,
842+ TimeSpan . FromMilliseconds ( 250 ) , DuplexMaximumProcessOutputCharacters ) with
843+ { Input = prompt } ;
844+ var stopwatch = Stopwatch . StartNew ( ) ;
845+ var action = async ( ) =>
846+ {
847+ await foreach ( var _ in new DefaultCodexProcessRunner ( ) . RunAsync ( invocation , NullLogger . Instance , CancellationToken . None ) )
848+ {
849+ }
850+ } ;
851+
852+ var exception = await Assert . That ( action ) . ThrowsException ( ) ;
853+ stopwatch . Stop ( ) ;
854+
855+ await Assert . That ( exception ) . IsTypeOf < TimeoutException > ( ) ;
856+ await Assert . That ( exception ) . IsNotTypeOf < CliExecutionFailureException > ( ) ;
857+ await Assert . That ( stopwatch . Elapsed < TimeSpan . FromSeconds ( 3 ) ) . IsTrue ( ) ;
858+ }
859+
811860 [ Test ]
812861 public async Task DefaultProcessRunner_CancellationWhileProcessStillRunning_ThrowsOperationCanceledException ( )
813862 {
0 commit comments