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]>
109 lines
8.1 KiB
JavaScript
109 lines
8.1 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import {broker} from '../../cli/tests/helpers.mjs';
|
|
import {views} from '../../bus/src/views.mjs';
|
|
import {startServer} from '../src/serve.mjs';
|
|
import {close} from './fixture.mjs';
|
|
import {browser} from './browser.mjs';
|
|
import {BusReadError} from '../src/bus.mjs';
|
|
|
|
function seed(t){
|
|
const s=broker(t),call=(verb,args={})=>s.b.request(s.reader,{verb,args});
|
|
const bus=views({call:async(...args)=>structuredClone(call(...args))});
|
|
const send=body=>s.b.request(s.coder,{verb:'message.send',args:{to:'human',body,class:'INFO'}}).id;
|
|
return {...s,bus,send};
|
|
}
|
|
test('messages HTTP read pages and preserves metadata; bad arguments refuse before a read; other reads unchanged',async t=>{
|
|
const s=seed(t),a=s.send('first'),b=s.send('second');
|
|
const web=await startServer({port:0,board:'http://127.0.0.1:9',bus:s.bus});t.after(()=>close(web));
|
|
const url=`http://127.0.0.1:${web.address().port}/api/bus/`;
|
|
const get=async path=>{const res=await fetch(url+path);return {status:res.status,body:await res.json()}};
|
|
const page=await get('messages?limit=1');assert.equal(page.status,200);
|
|
assert.deepEqual(page.body.rows.map(m=>m.id),[b]); assert.equal(page.body.total,2);assert.equal(page.body.unread,2);assert.equal(page.body.next,b);
|
|
const next=await get(`messages?after=${b}&limit=1`);assert.deepEqual(next.body.rows.map(m=>m.id),[a]);assert.equal(next.body.next,null);
|
|
for(const query of ['limit=0','limit=201','limit=1.5','limit=no','limit=','after=','after=%3Cb%3E','limit=2&limit=3','after=x&after=y','extra=1']){
|
|
const res=await get('messages?'+query);assert.equal(res.status,400,query);assert.equal(res.body.error,'invalid-request');
|
|
}
|
|
for(const verb of ['inbox','tasks','agents','replies?id='+a,'decisions?list=recent','credentials','trail?subject='+a]){
|
|
const res=await get(verb);assert.equal(res.status,200,verb);assert.deepEqual(Object.keys(res.body).sort(),['at','rows']);
|
|
const [name,query='']=verb.split('?'),params=new URLSearchParams(query);
|
|
const args=Object.fromEntries(params);
|
|
assert.deepEqual(res.body.rows,structuredClone(s.b.request(s.reader,{verb:name,args})),verb);
|
|
assert.match(res.body.at,/^\d{4}-.*Z$/);
|
|
}
|
|
});
|
|
test('Console Messages lists late unsolicited messages on polling with sender class time read state and trail; nav count includes beyond the page; reads never deliver', {timeout:45000},async t=>{
|
|
const s=seed(t),early=s.send('early');
|
|
const web=await startServer({port:0,board:'http://127.0.0.1:9',bus:s.bus});
|
|
const b=await browser({machine:false});
|
|
const wait=expr=>b.evaluate(`(async()=>{for(let i=0;i<200;i++){if(${expr})return true;await new Promise(r=>setTimeout(r,25))}throw Error('condition timed out')})()`);
|
|
try{
|
|
// Reach a real scheduled poll without making a test wait ten seconds.
|
|
await b.call('Page.addScriptToEvaluateOnNewDocument',{source:'const originalTimeout=window.setTimeout; window.setTimeout=(fn,ms,...args)=>originalTimeout(fn,ms===10000?100:ms,...args);'});
|
|
await b.navigate(`http://127.0.0.1:${web.address().port}/#/messages`);
|
|
await wait('document.querySelector("#message-list")');
|
|
const late=s.send('<img src=x onerror="window.injected=1"> late unsolicited');
|
|
const before=s.store.all('SELECT * FROM deliveries');
|
|
await wait('document.querySelector("#message-list")?.textContent.includes("late unsolicited")');
|
|
assert.deepEqual(s.store.all('SELECT * FROM deliveries'),before);
|
|
const items=await b.evaluate('[...document.querySelectorAll("#message-list li")].map(e=>({text:e.textContent,href:e.querySelector("a").getAttribute("href")}))');
|
|
assert.match(items[0].text,/coder.*INFO.*Unread/s);assert.match(await b.evaluate('document.querySelector("#message-list time").getAttribute("datetime")'), /^\d{4}-.*Z$/);
|
|
await b.viewport(400,900);
|
|
assert.equal(await b.evaluate('document.documentElement.scrollWidth <= document.documentElement.clientWidth'),true);
|
|
if(process.env.ROCKO_MESSAGES_SHOT) await b.screenshot(process.env.ROCKO_MESSAGES_SHOT);
|
|
assert.equal(items[0].href,`#/trail/message/${late}`);assert.equal(items[1].href,`#/trail/message/${early}`);
|
|
assert.equal(await b.evaluate('!!window.injected || !!document.querySelector("#message-list img")'),false);
|
|
for(let i=0;i<52;i++)s.send(`backlog ${i}`);
|
|
await wait(`document.querySelector('#sections a[href="#/messages"]')?.textContent.includes("54")`);
|
|
await wait('document.querySelectorAll("#message-list li").length===50');
|
|
assert.ok(await b.evaluate('!!document.querySelector("#bus-view a[data-older]")'));
|
|
await b.evaluate('document.querySelector("#bus-view a[data-older]").click(),true');
|
|
await wait('document.querySelectorAll("#message-list li").length===4');
|
|
assert.match(await b.evaluate('document.querySelector("#message-list").textContent'),/late unsolicited/);
|
|
const got=s.b.request(s.human,{verb:'message.receive'});
|
|
for(const m of got)s.b.request(s.human,{verb:'message.read',args:{id:m.id}});
|
|
await wait('document.querySelector("#message-list")?.textContent.includes("Read") && !document.querySelector("#message-list")?.textContent.includes("Unread")');
|
|
assert.equal(await b.evaluate(`document.querySelector('#sections a[href="#/messages"] .count').textContent`),'0');
|
|
}finally{await b.close();await close(web)}
|
|
});
|
|
|
|
|
|
test('Messages nav count polls on Tasks and Board and clears on a Board refusal; messages 403 refuses local pages', {timeout:45000}, async t=>{
|
|
const s=seed(t);s.send('one');s.send('two');
|
|
let messagesFailure=null;
|
|
const bus={...s.bus,messages:async args=>{if(messagesFailure)throw new BusReadError(messagesFailure);return s.bus.messages(args);}};
|
|
const at=()=>new Date().toISOString(), localReads=[];
|
|
const reads={
|
|
queue:async()=>{localReads.push('queue');return {rows:[],notes:[],at:at()};},
|
|
business:async()=>{localReads.push('business');return {business:{id:'demo',human:'jason',arbiters:{},projects:[],vars:{},instances:[],actions:[],credentials:[]},at:at()};},
|
|
settings:async()=>{localReads.push('settings');return {release:'0.0.1',business:'demo',notifier:{binding:null},at:at()};},
|
|
};
|
|
const web=await startServer({port:0,board:'http://127.0.0.1:9',bus,reads});
|
|
const b=await browser({machine:false});
|
|
const wait=expr=>b.evaluate(`(async()=>{for(let i=0;i<200;i++){if(${expr})return true;await new Promise(r=>setTimeout(r,25))}throw Error('condition timed out: '+${JSON.stringify(expr)})})()`);
|
|
const badge=`document.querySelector('#sections a[href="#/messages"] .count')`;
|
|
try{
|
|
await b.call('Page.addScriptToEvaluateOnNewDocument',{source:'const originalTimeout=window.setTimeout; window.setTimeout=(fn,ms,...args)=>originalTimeout(fn,ms===10000?100:ms,...args);'});
|
|
await b.navigate(`http://127.0.0.1:${web.address().port}/#/tasks`);
|
|
await wait(`document.querySelector('#bus-view h1')?.textContent==='Tasks' && ${badge}?.textContent==='2'`);
|
|
s.send('three');await wait(`${badge}?.textContent==='3'`);
|
|
await b.evaluate('location.hash="#/",true');
|
|
await wait(`document.querySelector('#bus-view').hidden && document.querySelector('#held')?.textContent.includes('coder') && ${badge}?.textContent==='3'`);
|
|
s.send('four');await wait(`${badge}?.textContent==='4'`);
|
|
messagesFailure='human-required';
|
|
await wait(`!${badge} && document.querySelector('#held')?.textContent.includes('refused')`);
|
|
messagesFailure=null;await wait(`${badge}?.textContent==='4'`);
|
|
for(const [path,title] of [['queue','Queue'],['business','Business demo'],['settings','Settings']]){
|
|
await b.evaluate(`location.hash=${JSON.stringify('#/'+path)},true`);
|
|
await wait(`document.querySelector('#bus-view h1')?.textContent===${JSON.stringify(title)} && ${badge}?.textContent==='4'`);
|
|
const before=localReads.length;
|
|
messagesFailure='human-required';
|
|
await wait(`!!document.querySelector('#bus-view .banner.ref') && !${badge}`);
|
|
assert.match(await b.evaluate('document.querySelector("#bus-view h1").textContent'),/Bus refused the read/);
|
|
assert.equal(localReads.length,before,'a refused messages pre-read prevents the local read');
|
|
messagesFailure=null;
|
|
await wait(`document.querySelector('#bus-view h1')?.textContent===${JSON.stringify(title)} && ${badge}?.textContent==='4'`);
|
|
}
|
|
}finally{await b.close();await close(web)}
|
|
});
|