Row 150 round 2, Darkwing's candidate (agents/darkwing/work/queue-150/r2/, manifest 7556dda0, 7 paths), a rebase-only round onto63c82f1c4after row 139 changed broker.mjs. Filbert approved round 1 (comment 28800) and the rebase (comment 28806): only broker.mjs's index line and hunk offset moved. Darkwing's r2 round ran 6 files, ROUND_PASSED. `mosaic inbox --read <id>...` and `--read-all` mark the human's unread messages read. Each argument is tried; refusals print their code and the command exits 2. The Console's send section now names `mosaic inbox` and `mosaic inbox --read`. broker.mjs changes a comment only. The public/ diff is sha256 096d922c, unchanged from round 1 and covered by Mos's decision 123 go. Sage's landing gate ran on4ebef2284plus the candidate: 25 steps, all rc 0 (15 packages, 10 script suites), 20:41:55Z to 20:46:24Z, network sage-r150g_default removed. Cherry-picked onto1ad48035c, which added only a docs/SESSIONS.md line. Co-Authored-By: Claude Opus 5.5 <[email protected]>
93 lines
6.0 KiB
JavaScript
93 lines
6.0 KiB
JavaScript
// Row 150 (#1644): mosaic inbox --read <id>... and --read-all mark the human's unread messages
|
|
// read through message.read, receiving an undelivered one first.
|
|
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});
|
|
const unread=s=>s.store.all("SELECT m.id FROM messages m WHERE to_role='human' AND NOT EXISTS(SELECT 1 FROM deliveries d WHERE d.message=m.id AND d.op='read') ORDER BY seq").map(r=>r.id);
|
|
const send=(s,body)=>call(s,s.coder,'message.send',{to:'human',body,class:'INFO'});
|
|
|
|
test('inbox --read marks one message by the id inbox prints, delivered or not, and leaves the rest unread',async t=>{
|
|
const s=broker(t),deps={system:{dataRoot:tmp(t)},transport:s.transport};
|
|
const skipped=send(s,'skipped by talk'), other=send(s,'stays unread');
|
|
call(s,s.human,'message.receive'); // what plain talk leaves: delivered, unread
|
|
const fresh=send(s,'never received'); // undelivered
|
|
const later=send(s,'arrives after');
|
|
const x=io();
|
|
await main(['inbox','--read',skipped.id.slice(0,8),'--business','demo'],x,deps);
|
|
assert.equal(x.out.text,`marked ${skipped.id.slice(0,8)}\nmarked 1 of 1\n`);
|
|
assert.deepEqual(unread(s),[other.id,fresh.id,later.id]);
|
|
assert.ok(!s.calls.some(c=>c.verb==='message.receive'),'a delivered message is read without receiving');
|
|
|
|
const y=io();
|
|
await main(['inbox','--read',fresh.id,'--business','demo'],y,deps);
|
|
assert.equal(y.out.text,`marked ${fresh.id.slice(0,8)}\nmarked 1 of 1\n`);
|
|
assert.deepEqual(unread(s),[other.id,later.id]);
|
|
// The receive that made it readable delivered `later` too: inbox still lists it, follow won't.
|
|
assert.deepEqual(call(s,s.human,'message.receive'),[]);
|
|
const z=io(); await main(['inbox','--business','demo'],z,deps);
|
|
assert.match(z.out.text,/messages: 2 unread/); assert.match(z.out.text,/arrives after/);
|
|
});
|
|
|
|
test('inbox --read refuses an id that is not the human\'s, prints its code, still marks the rest and exits 2',async t=>{
|
|
const s=broker(t),deps={system:{dataRoot:tmp(t)},transport:s.transport};
|
|
const mine=call(s,s.human,'message.send',{to:'coder',body:'to the coder'});
|
|
const a=send(s,'first'), b=send(s,'second');
|
|
const x=io();
|
|
await assert.rejects(main(['inbox','--read',a.id.slice(0,8),mine.id,'00000000',b.id.slice(0,12),'--business','demo'],x,deps),
|
|
e=>e.exitCode===2&&e.message==='2 of 4 not marked');
|
|
assert.equal(x.out.text,[`marked ${a.id.slice(0,8)}`,`not marked ${mine.id}: message-not-found`,'not marked 00000000: no-unread-match',`marked ${b.id.slice(0,8)}`,'marked 2 of 4',''].join('\n'));
|
|
assert.deepEqual(unread(s),[]);
|
|
assert.deepEqual(s.calls.filter(c=>c.verb==='message.read').map(c=>c.args.id).filter(id=>id===mine.id),[mine.id],'a full id goes to the broker as given');
|
|
});
|
|
|
|
test('inbox --read refuses an ambiguous or short prefix, names every match, reads nothing for them and marks the rest',async t=>{
|
|
const messages=[{id:'aaaa1111-0000-4000-8000-000000000001',read:false},{id:'aaaa1111-0000-4000-8000-000000000002',read:false},{id:'bbbb3333-0000-4000-8000-000000000003',read:false}];
|
|
const calls=[];
|
|
const transport=()=>async(verb,args={})=>{
|
|
calls.push({verb,args});
|
|
if(verb==='messages')return {messages,total:3,unread:3,next:null};
|
|
if(verb==='message.read')return {read:true};
|
|
throw new Error(`unexpected ${verb}`);
|
|
};
|
|
const x=io();
|
|
await assert.rejects(main(['inbox','--read','aaaa1111','bbbb3333','aaaa','','--business','demo'],x,{system:{dataRoot:tmp(t)},transport}),{exitCode:2});
|
|
assert.equal(x.out.text,[`not marked aaaa1111: ambiguous, matches ${messages[0].id}, ${messages[1].id}`,'marked bbbb3333','not marked aaaa: too-short, use at least 8 characters of the id','not marked : too-short, use at least 8 characters of the id','marked 1 of 4',''].join('\n'));
|
|
assert.deepEqual(calls.filter(c=>c.verb==='message.read').map(c=>c.args.id),[messages[2].id]);
|
|
});
|
|
|
|
test('inbox --read-all marks every message the inbox walk lists, delivered or not, and inbox then shows 0 unread',async t=>{
|
|
const s=broker(t),deps={system:{dataRoot:tmp(t)},transport:s.transport};
|
|
for(let i=0;i<120;i++)send(s,`early-${i}`);
|
|
call(s,s.human,'message.receive'); // 100 delivered, 20 not
|
|
for(let i=0;i<110;i++)send(s,`late-${i}`);
|
|
const x=io();
|
|
await main(['inbox','--read-all','--business','demo'],x,deps);
|
|
assert.equal(x.out.text,'marked 230 of 230\n');
|
|
assert.deepEqual(unread(s),[]);
|
|
assert.equal(s.calls.filter(c=>c.verb==='message.read').length,232,'one retry at each receive batch');
|
|
assert.equal(s.calls.filter(c=>c.verb==='message.receive').length,2);
|
|
const y=io(); await main(['inbox','--business','demo'],y,deps);
|
|
assert.match(y.out.text,/messages: 0 unread\n$/);
|
|
const z=io(); await main(['inbox','--read-all','--business','demo'],z,deps);
|
|
assert.equal(z.out.text,'marked 0 of 0\n');
|
|
});
|
|
|
|
test('inbox --read stops on an error that is not about one message',async t=>{
|
|
const transport=()=>async verb=>{
|
|
if(verb==='messages')return {messages:[{id:'cccc4444-0000-4000-8000-000000000004',read:false}],total:1,unread:1,next:null};
|
|
const e=new Error('outcome-unknown');e.code='outcome-unknown';e.exitCode=1;throw e;
|
|
};
|
|
const x=io();
|
|
await assert.rejects(main(['inbox','--read','cccc4444','--business','demo'],x,{system:{dataRoot:tmp(t)},transport}),{exitCode:1});
|
|
assert.equal(x.out.text,'');
|
|
});
|
|
|
|
test('inbox --read and --read-all refuse bad combinations; other verbs refuse them',async t=>{
|
|
const s=broker(t),deps={system:{dataRoot:tmp(t)},transport:s.transport};
|
|
for(const argv of [['inbox','--read'],['inbox','--read','--read-all'],['inbox','--read-all','abcd'],['inbox','--read','abcd','--json'],['inbox','--read-all','--json'],['inbox','--read','abcd','--tester'],['inbox','--read-all','--tester'],['tasks','--read','abcd'],['agents','--read-all']])
|
|
await assert.rejects(main([...argv,'--business','demo'],io(),deps),{exitCode:4},argv.join(' '));
|
|
assert.deepEqual(s.calls,[]);
|
|
});
|