Files
stack/packages/webui/tests/messages.test.mjs
T
jason.woltjeandClaude Opus 5.5 2b2a9e14ee bus, cli, webui: every message to the human, read state, Messages page (#1588)
Row 94, round 2. Rocko built it, Dewey approved it (comment 28104) on
manifest 53b01ea9 (16 paths, base e918a915). 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 on 645489e4 plus 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]>
2026-10-13 07:58:36 -05:00

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)}
});