Row 94, round 2. Rocko built it, Dewey approved it (comment 28104) on manifest 53b01ea9 (16 paths, basee918a915). The bus keeps a messages view with read state, where delivered but not read stays read:false. The CLI gains messages and follow, and inbox leaves out messages already read while inbox --json stays byte-compatible. The Console gets a Messages page and an unread count in the nav. Mos gave the go under decision 123 (public/ files). Sage gate on645489e4plus the patch, 12:52:14Z to 12:55:44Z: bus 115, cli 129, webui 104, all fail 0. Every scripts/test-*.sh exited 0 (auth 15, conductor 17, config 24, discord 66, extension-package 18, foundation 44, queue 27, release 14, task 98). Manifest 16/16 before and after. Co-Authored-By: Claude Opus 5.5 <[email protected]>
87 lines
5.1 KiB
JavaScript
87 lines
5.1 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import {main} from '../src/cli.mjs';
|
|
import {broker,io,tmp} from './helpers.mjs';
|
|
const call=(s,cap,verb,args={})=>s.b.request(cap,{verb,args});
|
|
for(const follow of [false,true]) test(follow?'talk --follow continues after reply and prints a later unsolicited message':'follow prints an unsolicited message after plain talk returned',async t=>{
|
|
const s=broker(t), x=io(), controller=new AbortController();
|
|
let polls=0,sent;
|
|
const transport=business=>{const inner=s.transport(business);return async(verb,args)=>{
|
|
if(verb==='message.send') {const result=await inner(verb,args);sent=result.id;return result;}
|
|
if(verb==='message.receive'){
|
|
polls++;
|
|
if(polls===1 && sent)call(s,s.coder,'message.send',{to:'human',body:'direct answer',in_reply_to:sent});
|
|
if(polls===2)call(s,s.coder,'message.send',{to:'human',body:'later unsolicited',class:'INFO'});
|
|
if(polls===3)controller.abort();
|
|
}
|
|
return inner(verb,args);
|
|
}};
|
|
const deps={system:{dataRoot:tmp(t)},transport,talkPollMs:5,signal:controller.signal};
|
|
if(!follow){await main(['talk','coder','question','--business','demo'],x,deps); assert.match(x.out.text,/direct answer/);}
|
|
await main(follow?['talk','coder','question','--follow','--business','demo']:['follow','--business','demo'],x,deps);
|
|
assert.equal(polls,3); assert.match(x.out.text,/later unsolicited/);
|
|
assert.equal((x.out.text.match(/later unsolicited/g)||[]).length,1);
|
|
assert.deepEqual(call(s,s.human,'message.receive'),[]);
|
|
assert.equal(s.calls.filter(c=>c.verb==='message.send').length,1);
|
|
assert.equal(s.calls.filter(c=>c.verb==='message.read').length,2);
|
|
});
|
|
test('inbox lists all unread human messages under decisions, does not deliver or read them',async t=>{
|
|
const s=broker(t),x=io(),deps={system:{dataRoot:tmp(t)},transport:s.transport};
|
|
for(let i=0;i<205;i++)call(s,s.coder,'message.send',{to:'human',body:`unsolicited-${i}`,class:'INFO'});
|
|
const delivered=call(s,s.human,'message.receive');
|
|
call(s,s.human,'message.read',{id:delivered[0].id});
|
|
const before=s.store.all('SELECT * FROM deliveries');
|
|
await main(['inbox','--business','demo'],x,deps);
|
|
assert.match(x.out.text,/inbox:/);assert.match(x.out.text,/messages: 204 unread/);
|
|
assert.doesNotMatch(x.out.text,/unsolicited-0\n/);
|
|
assert.match(x.out.text,/unsolicited-1\n/);assert.match(x.out.text,/unsolicited-204\n/);
|
|
assert.deepEqual(s.store.all('SELECT * FROM deliveries'),before);
|
|
const json=io(); await main(['inbox','--business','demo','--json'],json,deps);
|
|
const value=JSON.parse(json.out.text);assert.deepEqual(value,[]);assert.equal(json.out.text,JSON.stringify(s.read("inbox"),null,2)+"\n");
|
|
});
|
|
test('follow refuses agent context and bad arguments',async t=>{
|
|
const s=broker(t),deps={system:{dataRoot:tmp(t)},transport:s.transport};
|
|
for(const argv of [['follow','extra'],['follow','--wait','1'],['talk','coder','hi','--follow','--wait','1']])await assert.rejects(main(argv,io(),deps),{exitCode:4});
|
|
await assert.rejects(main(['follow','--business','demo'],io({...io().env,CLAUDECODE:'1'}),deps),{exitCode:3});
|
|
});
|
|
|
|
test('follow Ctrl-C wakes the poll sleep and removes its handler; transport errors also clean up', async t => {
|
|
const s = broker(t), x = io(), before = process.listenerCount('SIGINT'), cleanup = new AbortController();
|
|
let received;
|
|
const ready = new Promise(resolve => { received = resolve; });
|
|
const deps = { system: { dataRoot: tmp(t) }, talkPollMs: 60000, signal: cleanup.signal,
|
|
transport: () => async verb => { assert.equal(verb, 'message.receive'); received(); return []; } };
|
|
const running = main(['follow', '--business', 'demo'], x, deps);
|
|
await ready;
|
|
// Let the command install its sleep's abort listener before emitting Ctrl-C.
|
|
await new Promise(resolve => setImmediate(resolve));
|
|
process.emit('SIGINT');
|
|
try {
|
|
await Promise.race([running, new Promise((_, reject) => {
|
|
const timer = setTimeout(() => reject(Error('Ctrl-C did not wake follow')), 500);
|
|
timer.unref();
|
|
})]);
|
|
} finally { cleanup.abort(); await running; }
|
|
assert.equal(process.listenerCount('SIGINT'), before);
|
|
await assert.rejects(main(['follow', '--business', 'demo'], io(), {
|
|
...deps, signal: undefined, transport: () => async () => { throw Error('transport failed'); },
|
|
}), /transport failed/);
|
|
assert.equal(process.listenerCount('SIGINT'), before);
|
|
});
|
|
|
|
|
|
test('plain talk leaves the process SIGINT handlers unchanged',async t=>{
|
|
const s=broker(t),before=process.listeners('SIGINT'),x=io();let sent;
|
|
const deps={system:{dataRoot:tmp(t)},transport:business=>{const inner=s.transport(business);return async(verb,args)=>{
|
|
if(verb==='message.send'){const result=await inner(verb,args);sent=result.id;return result;}
|
|
if(verb==='message.receive'){
|
|
assert.deepEqual(process.listeners('SIGINT'),before);
|
|
call(s,s.coder,'message.send',{to:'human',body:'answer',in_reply_to:sent});
|
|
}
|
|
return inner(verb,args);
|
|
}}};
|
|
try {await main(['talk','coder','question','--business','demo'],x,deps);}
|
|
finally {for(const handler of process.listeners('SIGINT'))if(!before.includes(handler))process.removeListener('SIGINT',handler);}
|
|
assert.deepEqual(process.listeners('SIGINT'),before);
|
|
});
|