// Row 150 (#1644): mosaic inbox --read ... 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,[]); });