@@ -1043,6 +1043,83 @@ describe('UnixLocalSandboxClient', () => {
10431043 expect ( output ) . not . toContain ( 'tty no' ) ;
10441044 } ) ;
10451045
1046+ it ( 'fails tty commands clearly when the Python PTY bridge is unavailable' , async ( ) => {
1047+ const originalPython = process . env . OPENAI_AGENTS_PYTHON ;
1048+ const missingPython = join ( rootDir , 'missing-python3' ) ;
1049+ process . env . OPENAI_AGENTS_PYTHON = missingPython ;
1050+
1051+ try {
1052+ const client = new UnixLocalSandboxClient ( {
1053+ workspaceBaseDir : rootDir ,
1054+ } ) ;
1055+ const session = await client . create ( new Manifest ( ) ) ;
1056+
1057+ await expect (
1058+ session . execCommand ( {
1059+ cmd : 'printf "hello\\n"' ,
1060+ shell : '/bin/sh' ,
1061+ login : false ,
1062+ tty : true ,
1063+ yieldTimeMs : 1_000 ,
1064+ } ) ,
1065+ ) . rejects . toMatchObject ( {
1066+ code : 'configuration_error' ,
1067+ message :
1068+ 'PTY support requires Python 3. Install python3 or set OPENAI_AGENTS_PYTHON to a Python 3 executable.' ,
1069+ details : expect . objectContaining ( {
1070+ pythonExecutable : missingPython ,
1071+ } ) ,
1072+ } ) ;
1073+ } finally {
1074+ if ( originalPython === undefined ) {
1075+ delete process . env . OPENAI_AGENTS_PYTHON ;
1076+ } else {
1077+ process . env . OPENAI_AGENTS_PYTHON = originalPython ;
1078+ }
1079+ }
1080+ } ) ;
1081+
1082+ it ( 'checks relative PTY Python executables from the command cwd' , async ( ) => {
1083+ const originalPython = process . env . OPENAI_AGENTS_PYTHON ;
1084+ const pythonPath = await whichPython ( ) ;
1085+
1086+ try {
1087+ const client = new UnixLocalSandboxClient ( {
1088+ workspaceBaseDir : rootDir ,
1089+ } ) ;
1090+ const session = await client . create ( new Manifest ( ) ) ;
1091+ await symlink (
1092+ pythonPath ,
1093+ join ( session . state . workspaceRootPath , 'python3' ) ,
1094+ ) ;
1095+ process . env . OPENAI_AGENTS_PYTHON = './python3' ;
1096+
1097+ const output = await session . execCommand ( {
1098+ cmd : 'test -t 0 && printf "tty yes\\n" || printf "tty no\\n"' ,
1099+ shell : '/bin/sh' ,
1100+ login : false ,
1101+ tty : true ,
1102+ yieldTimeMs : 1_000 ,
1103+ } ) ;
1104+ const finalOutput = await collectActiveCommandOutput (
1105+ {
1106+ writeStdin : ( args ) => session . writeStdin ( args ) ,
1107+ } ,
1108+ output ,
1109+ ) ;
1110+
1111+ expect ( finalOutput ) . toContain ( 'Process exited with code 0' ) ;
1112+ expect ( finalOutput ) . toContain ( 'tty yes' ) ;
1113+ expect ( finalOutput ) . not . toContain ( 'tty no' ) ;
1114+ } finally {
1115+ if ( originalPython === undefined ) {
1116+ delete process . env . OPENAI_AGENTS_PYTHON ;
1117+ } else {
1118+ process . env . OPENAI_AGENTS_PYTHON = originalPython ;
1119+ }
1120+ }
1121+ } ) ;
1122+
10461123 it ( 'accepts absolute sandbox paths when the manifest root is slash' , async ( ) => {
10471124 const client = new UnixLocalSandboxClient ( {
10481125 workspaceBaseDir : rootDir ,
@@ -2160,6 +2237,11 @@ async function writeUntilExit(
21602237 return combinedOutput ;
21612238}
21622239
2240+ async function whichPython ( ) : Promise < string > {
2241+ const { stdout } = await execFileAsync ( 'which' , [ 'python3' ] ) ;
2242+ return stdout . trim ( ) ;
2243+ }
2244+
21632245async function collectActiveCommandOutput (
21642246 session : {
21652247 writeStdin ( args : {
0 commit comments