@@ -12,6 +12,7 @@ use codex_exec_server::ExecProcess;
1212use codex_exec_server:: ProcessId ;
1313use codex_exec_server:: ReadResponse ;
1414use codex_exec_server:: StartedExecProcess ;
15+ use codex_exec_server:: WriteStatus ;
1516use pretty_assertions:: assert_eq;
1617use test_case:: test_case;
1718use tokio:: sync:: watch;
@@ -54,6 +55,7 @@ async fn assert_exec_process_starts_and_exits(use_remote: bool) -> Result<()> {
5455 env_policy : /*env_policy*/ None ,
5556 env : Default :: default ( ) ,
5657 tty : false ,
58+ pipe_stdin : false ,
5759 arg0 : None ,
5860 } )
5961 . await ?;
@@ -131,6 +133,7 @@ async fn assert_exec_process_streams_output(use_remote: bool) -> Result<()> {
131133 env_policy : /*env_policy*/ None ,
132134 env : Default :: default ( ) ,
133135 tty : false ,
136+ pipe_stdin : false ,
134137 arg0 : None ,
135138 } )
136139 . await ?;
@@ -164,6 +167,7 @@ async fn assert_exec_process_write_then_read(use_remote: bool) -> Result<()> {
164167 env_policy : /*env_policy*/ None ,
165168 env : Default :: default ( ) ,
166169 tty : true ,
170+ pipe_stdin : false ,
167171 arg0 : None ,
168172 } )
169173 . await ?;
@@ -184,6 +188,73 @@ async fn assert_exec_process_write_then_read(use_remote: bool) -> Result<()> {
184188 Ok ( ( ) )
185189}
186190
191+ async fn assert_exec_process_write_then_read_without_tty ( use_remote : bool ) -> Result < ( ) > {
192+ let context = create_process_context ( use_remote) . await ?;
193+ let process_id = "proc-stdin-pipe" . to_string ( ) ;
194+ let session = context
195+ . backend
196+ . start ( ExecParams {
197+ process_id : process_id. clone ( ) . into ( ) ,
198+ argv : vec ! [
199+ "/bin/sh" . to_string( ) ,
200+ "-c" . to_string( ) ,
201+ "IFS= read line; printf 'from-stdin:%s\\ n' \" $line\" " . to_string( ) ,
202+ ] ,
203+ cwd : std:: env:: current_dir ( ) ?,
204+ env_policy : /*env_policy*/ None ,
205+ env : Default :: default ( ) ,
206+ tty : false ,
207+ pipe_stdin : true ,
208+ arg0 : None ,
209+ } )
210+ . await ?;
211+ assert_eq ! ( session. process. process_id( ) . as_str( ) , process_id) ;
212+
213+ tokio:: time:: sleep ( Duration :: from_millis ( 200 ) ) . await ;
214+ let write_response = session. process . write ( b"hello\n " . to_vec ( ) ) . await ?;
215+ assert_eq ! ( write_response. status, WriteStatus :: Accepted ) ;
216+ let StartedExecProcess { process } = session;
217+ let wake_rx = process. subscribe_wake ( ) ;
218+ let actual = collect_process_output_from_reads ( process, wake_rx) . await ?;
219+
220+ assert_eq ! ( actual, ( "from-stdin:hello\n " . to_string( ) , Some ( 0 ) , true ) ) ;
221+ Ok ( ( ) )
222+ }
223+
224+ async fn assert_exec_process_rejects_write_without_pipe_stdin ( use_remote : bool ) -> Result < ( ) > {
225+ let context = create_process_context ( use_remote) . await ?;
226+ let process_id = "proc-stdin-closed" . to_string ( ) ;
227+ let session = context
228+ . backend
229+ . start ( ExecParams {
230+ process_id : process_id. clone ( ) . into ( ) ,
231+ argv : vec ! [
232+ "/bin/sh" . to_string( ) ,
233+ "-c" . to_string( ) ,
234+ "sleep 0.3; if IFS= read -r line; then printf 'read:%s\\ n' \" $line\" ; else printf 'eof\\ n'; fi" . to_string( ) ,
235+ ] ,
236+ cwd : std:: env:: current_dir ( ) ?,
237+ env_policy : /*env_policy*/ None ,
238+ env : Default :: default ( ) ,
239+ tty : false ,
240+ pipe_stdin : false ,
241+ arg0 : None ,
242+ } )
243+ . await ?;
244+ assert_eq ! ( session. process. process_id( ) . as_str( ) , process_id) ;
245+
246+ let write_response = session. process . write ( b"ignored\n " . to_vec ( ) ) . await ?;
247+ assert_eq ! ( write_response. status, WriteStatus :: StdinClosed ) ;
248+ let StartedExecProcess { process } = session;
249+ let wake_rx = process. subscribe_wake ( ) ;
250+ let ( output, exit_code, closed) = collect_process_output_from_reads ( process, wake_rx) . await ?;
251+
252+ assert_eq ! ( output, "eof\n " ) ;
253+ assert_eq ! ( exit_code, Some ( 0 ) ) ;
254+ assert ! ( closed) ;
255+ Ok ( ( ) )
256+ }
257+
187258async fn assert_exec_process_preserves_queued_events_before_subscribe (
188259 use_remote : bool ,
189260) -> Result < ( ) > {
@@ -201,6 +272,7 @@ async fn assert_exec_process_preserves_queued_events_before_subscribe(
201272 env_policy : /*env_policy*/ None ,
202273 env : Default :: default ( ) ,
203274 tty : false ,
275+ pipe_stdin : false ,
204276 arg0 : None ,
205277 } )
206278 . await ?;
@@ -234,6 +306,7 @@ async fn remote_exec_process_reports_transport_disconnect() -> Result<()> {
234306 env_policy : /*env_policy*/ None ,
235307 env : Default :: default ( ) ,
236308 tty : false ,
309+ pipe_stdin : false ,
237310 arg0 : None ,
238311 } )
239312 . await ?;
@@ -289,6 +362,24 @@ async fn exec_process_write_then_read(use_remote: bool) -> Result<()> {
289362 assert_exec_process_write_then_read ( use_remote) . await
290363}
291364
365+ #[ test_case( false ; "local" ) ]
366+ #[ test_case( true ; "remote" ) ]
367+ #[ tokio:: test( flavor = "multi_thread" , worker_threads = 2 ) ]
368+ // Serialize tests that launch a real exec-server process through the full CLI.
369+ #[ serial_test:: serial( remote_exec_server) ]
370+ async fn exec_process_write_then_read_without_tty ( use_remote : bool ) -> Result < ( ) > {
371+ assert_exec_process_write_then_read_without_tty ( use_remote) . await
372+ }
373+
374+ #[ test_case( false ; "local" ) ]
375+ #[ test_case( true ; "remote" ) ]
376+ #[ tokio:: test( flavor = "multi_thread" , worker_threads = 2 ) ]
377+ // Serialize tests that launch a real exec-server process through the full CLI.
378+ #[ serial_test:: serial( remote_exec_server) ]
379+ async fn exec_process_rejects_write_without_pipe_stdin ( use_remote : bool ) -> Result < ( ) > {
380+ assert_exec_process_rejects_write_without_pipe_stdin ( use_remote) . await
381+ }
382+
292383#[ test_case( false ; "local" ) ]
293384#[ test_case( true ; "remote" ) ]
294385#[ tokio:: test( flavor = "multi_thread" , worker_threads = 2 ) ]
0 commit comments