import { spawn } from 'node:child_process'; import { mkdtemp,mkdir,rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { OpenCode } from '@opencode/client'; // Opt in: RUN_HOST_QUESTION_NATIVE_SMOKE=1 bun tests/integration/hostQuestionNativeSmoke.ts // Uses only owned temporary roots and resume:false; never calls a provider. if (process.env.RUN_HOST_QUESTION_NATIVE_SMOKE !== "1") { console.log("Skipped native host-question smoke; set RUN_HOST_QUESTION_NATIVE_SMOKE=1 to run."); process.exit(0); } const root=await mkdtemp(join(tmpdir(),'tjwater-native-question-')); await mkdir(join(root,'home')); await mkdir(join(root,'workspace')); let child:any; async function start(){ child=spawn(process.env.OPENCODE_COMMAND ?? 'opencode',['serve','--hostname=127.0.0.1','--port=0'],{cwd:root,env:{PATH:process.env.PATH,HOME:join(root,'home'),XDG_CONFIG_HOME:join(root,'config'),XDG_DATA_HOME:join(root,'data'),XDG_CACHE_HOME:join(root,'cache'),OPENCODE_DB:join(root,'native.sqlite'),OPENCODE_CONFIG_CONTENT:JSON.stringify({})},stdio:['ignore','pipe','pipe']}); const endpoint=await new Promise((resolve,reject)=>{let output='';const timer=setTimeout(()=>reject(new Error('startup timeout')),20000);child.on('exit',()=>reject(new Error('startup exit')));child.stderr.resume();child.stdout.on('data',(chunk:any)=>{output+=chunk;const url=/server listening on (https?:\/\/\S+)/.exec(output)?.[1];const password=/server password (\S+)/.exec(output)?.[1];if(url&&password){clearTimeout(timer);resolve({url,password});}});}); return OpenCode.make({baseUrl:endpoint.url,headers:{authorization:`Basic ${Buffer.from(`opencode:${endpoint.password}`).toString('base64')}`}}); } async function stop(){if(!child)return; const closing=new Promise(r=>child.once('close',()=>r()));child.kill('SIGTERM');await closing;child=undefined;} try{ let client=await start(); const session=await client.session.create({title:'No-inference host question smoke',location:{directory:join(root,'workspace')}}); const input={sessionID:session.id,id:'msg_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',text:'Durable fixed-ID user answer',resume:false}; await client.session.prompt(input); let duplicate='accepted';try{await client.session.prompt(input);}catch(e:any){duplicate=e?._tag??e?.name??'rejected';} const queued=await client.session.inbox.list({sessionID:session.id}); if(queued.filter((x:any)=>x.id===input.id).length!==1)throw new Error('fixed input duplicated'); if((await client.session.active())[session.id])throw new Error('resume:false started inference'); const form=await client.session.form.create({sessionID:session.id,title:'Smoke question',fields:[{key:'q',type:'string',required:true}]}); const before=await client.session.form.get({sessionID:session.id,formID:form.id}); if(before.state.status !== 'pending') throw new Error('new form was not pending'); await client.session.form.reply({sessionID:session.id,formID:form.id,answer:{q:'answer'}}); let terminal='unknown';try{terminal=(await client.session.form.get({sessionID:session.id,formID:form.id})).state.status;}catch(e:any){terminal=e?._tag;} const cancelled=await client.session.form.create({sessionID:session.id,title:'Cancel smoke',fields:[{key:'q',type:'string'}]}); await client.session.form.cancel({sessionID:session.id,formID:cancelled.id}); let cancelledState='unknown';try{cancelledState=(await client.session.form.get({sessionID:session.id,formID:cancelled.id})).state.status;}catch(e:any){cancelledState=e?._tag;} await stop();client=await start(); const recovered=await client.session.inbox.list({sessionID:session.id}); if(recovered.filter((x:any)=>x.id===input.id).length!==1)throw new Error('fixed input lost after restart'); let restored='unknown';try{restored=(await client.session.form.get({sessionID:session.id,formID:form.id})).state.status;}catch(e:any){restored=e?._tag;} console.log(JSON.stringify({native:'current isolated executable',fixed_id_unique:true,duplicate_result:duplicate,restart_durable:true,form_pending:before.state.status,form_after_reply:terminal,form_after_cancel:cancelledState,form_after_restart:restored,inference_started:false})); }catch(e:any){console.log(JSON.stringify({error:e?.message,tag:e?._tag,name:e?.name}));process.exitCode=1;}finally{await stop();await rm(root,{recursive:true,force:true});}