// World HTTP clients exercise the live chat API, including disconnects. import { BACKGROUND_CONTEXT as context } from '@earendil-works/chord/context'; import { fauxAssistantMessage, fauxToolCall } from '@earendil-works/pi-ai/providers/faux'; import { fixture } from './goal-questions-fixture'; import { assert, until } from './goal-questions-test-support'; import { harnessApi } from './harness-api'; import { serve } from './input'; const f=await fixture(); const harness=f.opened.harness; const api=harnessApi(harness,f.host.models(),f.loader,context); const serving=serve(harness,context,undefined,api); const curl=async(path:string,body?:any)=>{ const result=await f.host.exec(['/bin/curl.wasm',...(body===undefined?[]:['--json',JSON.stringify(body)]),`http://host.sausage/harness${path}`],{cwd:'/'}); assert(result.code===0,`curl failed: ${result.stderr}`); return JSON.parse(result.stdout); }; const id=await f.ultimate(); const question=await f.ask(id,{text:'Choose.',questions:[{text:'Name',kind:'text',ref:'1'}]}); assert(question.message,'Question fixture'); const listed=await curl('/conversations'); assert(listed.conversations.some((c:any)=>c.id===id && c.agent && !c.running),'Conversation list'); const initial=await curl(`/conversations/${id}/entries`); const after=initial.entries.at(-1).id; const answer=await curl('/extensions/questions/actions/answer',{args:{messageId:question.message.id,answers:[{question:'Name',answer:'API choice'}]}}); assert(!answer.error,JSON.stringify(answer)); await f.settled(); assert(JSON.stringify(await f.entries(id)).includes('API choice'),'Question answer reaches asker'); assert((await curl('/models')).models.length>0,'Models'); assert((await curl('/families')).families.default,'Families'); assert((await curl('/extensions')).good,'Extension status'); assert(Array.isArray((await curl('/credential-requests')).requests),'Credential requests'); assert((await curl('/unknown')).error,'Unknown route'); assert((await curl('/extensions/questions/actions/answerText',{})).error,'Only user actions are callable'); assert((await curl(`/conversations/${id}/entries?after=bad`)).error,'Invalid cursor'); assert((await curl(`/conversations/${id}/submit`,{content:'bad'})).error,'Missing request id'); const controller=new AbortController();let received=''; const stream=f.host.exec(['/bin/harness-api-client.wasm',`/harness/conversations/${id}/stream?after=${after}`],{cwd:'/',signal:controller.signal,onOutput:(text:string)=>{received+=text;}}).catch(()=>{}); let release!:()=>void; const held=new Promise(resolve=>release=resolve); f.hear(async(text:string)=>{if(text==='HTTP input')await held;}); const submitted=await curl(`/conversations/${id}/submit`,{content:'HTTP input',requestId:'http-input'}); assert(submitted.submissionId,'Submission acknowledgement'); const duplicate=await curl(`/conversations/${id}/submit`,{content:'HTTP input',requestId:'http-input'}); assert(duplicate.submissionId===submitted.submissionId,'Idempotent submission'); await until(()=>received.includes('HTTP input')); const during=await curl('/conversations'); assert(during.conversations.find((c:any)=>c.id===id).running,'Stream works during a turn'); release();await f.settled(); await until(()=>received.includes('pi.assistant')); controller.abort();await stream; const updated=await curl(`/conversations/${id}/entries?after=${after}`); assert(updated.entries.every((e:any)=>e.id>after),'Exclusive cursor'); assert(updated.entries.filter((e:any)=>e.kind==='pi.user'&&JSON.stringify(e).includes('HTTP input')).length===1,'No second input'); let stopping=false; f.faux.setResponses([async (_transcript:any,options:any)=>{ stopping=true; await new Promise(resolve=>options.signal.addEventListener('abort',resolve,{once:true})); return fauxAssistantMessage('Interrupted'); }]); await curl(`/conversations/${id}/submit`,{content:'Stop this turn',requestId:'stop-input'}); await until(()=>stopping); assert((await curl(`/conversations/${id}/stop`,{})).ok,'Stop'); await f.settled(); assert(!(await curl('/conversations')).conversations.find((c:any)=>c.id===id).running,'Stop ends active turn'); const eventsController=new AbortController();let events=''; const eventsStream=f.host.exec(['/bin/harness-api-client.wasm','/harness/events?after=0'],{cwd:'/',signal:eventsController.signal,onOutput:(text:string)=>{events+=text;}}).catch(()=>{}); const respond=(transcript:any):any=>{ f.faux.appendResponses([respond]); const last=transcript.messages.at(-1); const text=typeof last.content==='string'?last.content:JSON.stringify(last.content); return last.role==='user'&&text.includes('DELEGATE')?fauxAssistantMessage([fauxToolCall('subagent',{action:'start',agent:'coordinator',task:'facts'})],{stopReason:'toolUse'}):fauxAssistantMessage('Child answer'); }; f.faux.setResponses([respond]); await f.type(f.opened.root.id,'DELEGATE');await f.settled(); await until(()=>events.includes('subagents.started')); eventsController.abort();await eventsStream; let shutdownData=''; const shutdownStream=f.host.exec(['/bin/harness-api-client.wasm','/harness/events?after=0'],{cwd:'/',onOutput:(text:string)=>{shutdownData+=text;}}); await until(()=>shutdownData.includes('subagents.started')); await f.host.fs.write('/harness-test-done','ok'); await serving; await api.close(); assert((await shutdownStream).code===0,'Harness shutdown ends streams'); await f.opened.close();