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]>
This commit is contained in:
2026-10-13 07:58:36 -05:00
co-authored by Claude Opus 5.5
parent 645489e4cc
commit 2b2a9e14ee
16 changed files with 441 additions and 42 deletions
+59
View File
@@ -0,0 +1,59 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { Broker } from '../src/broker.mjs';
import { BUSINESSES, broker } from '../../cli/tests/helpers.mjs';
import { views } from '../src/views.mjs';
const call = (s, cap, verb, args = {}) => s.b.request(cap, { verb, args });
const send = (s, body, to = 'human') => call(s, s.coder, 'message.send', { to, body, class: 'INFO' }).id;
test('messages pages all human messages newest first without writes; read state is history', t => {
const s = broker(t), ids = [];
for (let i = 0; i < 205; i++) ids.push(send(s, `note ${i}`));
send(s, 'role only', 'pm');
const before = s.store.all('SELECT * FROM deliveries');
const first = s.read('messages');
assert.equal(first.messages.length, 50);
assert.equal(first.total, 205); assert.equal(first.unread, 205);
assert.deepEqual(first.messages.map(m => m.id), ids.slice(-50).reverse());
assert.ok(first.messages.every(m => m.read === false && m.in_reply_to === null));
assert.equal(first.next, ids[155]);
const second = s.read('messages', { limit: 200, after: first.next });
assert.deepEqual(second.messages.map(m => m.id), ids.slice(0,155).reverse());
assert.equal(second.next, null);
assert.equal(s.read('messages', {limit:200}).messages.length,200);
assert.deepEqual(s.store.all('SELECT * FROM deliveries'), before);
assert.deepEqual(call(s,s.human,'messages'), first);
const got = call(s,s.human,'message.receive');
call(s,s.human,'message.read',{id:got[0].id});
const read = s.read('messages', {limit:200, after:ids[1]});
assert.equal(read.messages[0].read,true); assert.equal(read.unread,204);
const delivered = s.read('messages', {limit:200, after:ids[2]}).messages.find(m => m.id === got[1].id);
assert.equal(delivered.read,false,'delivered without a read receipt remains unread');
});
test('messages refuses role callers, invalid paging and cursors outside this human feed', t => {
const s=broker(t); send(s,'hello'); const other=send(s,'role','pm');
for (const args of [{limit:0},{limit:201},{limit:1.5},{limit:'2'},{after:''},{after:3},{extra:true}])
assert.throws(()=>s.read('messages',args),{code:'invalid-request'});
for (const after of ['missing',other]) assert.throws(()=>s.read('messages',{after}),{code:'message-not-found'});
assert.throws(()=>call(s,s.coder,'messages'),{code:'human-required'});
const cap=s.b.bindReader({business:'demo'});
assert.throws(()=>call(s,cap,'message.receive'),{code:'read-only'});
});
test('views messages passes only its paging arguments through', async()=>{
const calls=[]; const v=views({call:async(...args)=>(calls.push(args),{messages:[],next:null})});
await v.messages({limit:2,after:'m-1'}); await v.messages();
assert.deepEqual(calls,[['messages',{limit:2,after:'m-1'}],['messages',{}]]);
});
test('messages isolates businesses even with a valid foreign cursor', t => {
const s=broker(t);
// A second broker shares this test's store but binds a separate business.
const otherBroker=new Broker({store:s.store,businesses:{other:{...BUSINESSES.demo,id:'other'}}});
const agent=otherBroker.bindLaunch({business:'other',role:'coder',run:'other-run',harness:'pi'});
otherBroker.request(agent,{verb:'role.claim'});
const foreign=otherBroker.request(agent,{verb:'message.send',args:{to:'human',body:'foreign'}}).id;
const own=send(s,'own');
assert.deepEqual(s.read('messages').messages.map(m=>m.id),[own]);
assert.throws(()=>s.read('messages',{after:foreign}),{code:'message-not-found'});
const reader=otherBroker.bindReader({business:'other'});
assert.deepEqual(otherBroker.request(reader,{verb:'messages'}).messages.map(m=>m.id),[foreign]);
});