Files
sausage/host/js/harness-api-test.ts
T

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.mainAgent();
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.main_agent.name==='mainAgent'&&listed.main_agent.conversationId===id,'Conversation list identifies the configured mainAgent 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();