86 lines
5.5 KiB
TypeScript
86 lines
5.5 KiB
TypeScript
// 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.standing();
|
|
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');
|
|
assert(listed.standing.name==='standing'&&listed.standing.conversationId===id,'Conversation list identifies the configured standing conversation');
|
|
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')).error,'The obsolete families route is absent');
|
|
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<void>(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();
|