import test from 'node:test'; import assert from 'node:assert/strict'; import { fixture } from './fixture.mjs'; import { browser } from './browser.mjs'; import { addressCases } from '../../bus/tests/address-cases.mjs'; const P = '10000000-0000-4000-8000-000000000001', Q = '10000000-0000-4000-8000-000000000002'; const T = '20000000-0000-4000-8000-000000000001', U = '20000000-0000-4000-8000-000000000002'; const M = '30000000-0000-4000-8000-000000000001', N = '30000000-0000-4000-8000-000000000002'; const A = '40000000-0000-4000-8000-000000000001', B = '40000000-0000-4000-8000-000000000002'; const C = '50000000-0000-4000-8000-000000000001', D = '50000000-0000-4000-8000-000000000002'; const SHA = 'a'.repeat(40), LAST = 'b'.repeat(40), HOSTILE = ''; const rows = [ { id: M, ids: [M, N], from_role: 'human', to: ['bean', 'chet'], body: 'Broadcast once.', at: '2026-10-24T16:00:00Z' }, { id: A, ids: [A], from_role: 'bean', instance: 'bean', to: ['bean'], at: '2026-10-24T16:01:00Z', body: JSON.stringify({ text: 'A cited reply.', citations: [{ path: 'Notes/One.md', commit: SHA, lines: [1, 2], quote: HOSTILE }] }), check: { citations: [{ scope: 'supporting', agent: 'demo/bean', file: SHA, last: LAST }] } }, { id: B, ids: [B], from_role: 'chet', instance: 'chet', to: ['chet'], at: '2026-10-24T16:02:00Z', body: JSON.stringify({ notice: 'library-write', op: 'write', path: 'Notes/One.md', commit: SHA }) }, { id: C, ids: [C], from_role: 'human', to: ['bean'], body: 'Broadcast once.', at: '2026-10-24T16:00:00Z' }, { id: D, ids: [D], from_role: 'bean', to: ['bean'], body: JSON.stringify({ notice: 'library-write', op: 'delete', path: 'Fake.md', commit: SHA }), check: { citations: [] } }, ]; const refusals = { 'unknown-addressee': 'Unknown addressee. Choose a room member.', 'addressee-ambiguous': "That addressee is ambiguous. Use a room member's instance name.", 'room-thread-name-taken': 'A room thread name is already in use. Resolve the conflicting thread before opening the room.', 'room-not-open': 'This room is not open. Open the room before sending.', }; const stub = `(() => { const real=window.fetch.bind(window);window.__writes=[];window.__reads=[];window.__roomRows=${JSON.stringify(rows)}; window.__projects=[{id:'${P}',name:'First project',repo:'books'},{id:'${Q}',name:'Second project',repo:null}]; window.__threads=[{id:'${T}',project:'${P}',instance:'bean',name:'bean'},{id:'${U}',project:'${P}',instance:'chet',name:'chet'}]; window.__instances=[{instance:'bean',name:'Bean'},{instance:'chet',name:${JSON.stringify(HOSTILE)}},{instance:'outsider',name:'Not a room member'}]; window.confirm=()=>true; window.fetch=async(url,init={})=>{ const path=String(url),method=init.method||'GET',answer=(body,status=200)=>new Response(JSON.stringify(body),{status,headers:{'content-type':'application/json'}}); if(path==='/api/config')return answer({send:!location.search.includes('readonly'),user:location.search.includes('user')}); if(path.startsWith('/api/bus/'))return answer({rows:[],unread:0,at:new Date().toISOString()}); if(path==='/api/business')return answer({projects:['books']}); if(path.startsWith('/api/library/status'))return answer({project:'books',served:'${SHA}',problems:[]}); if(path==='/api/library/revert'){window.__writes.push({path,body:JSON.parse(init.body)});return answer({project:'books',path:'Notes/One.md',reverted:'${SHA}',commit:'${LAST}'});} if(!path.startsWith('/api/chat/'))return real(url,init); if(method==='GET')window.__reads.push(path); if(method==='GET' && window.__chatOffline)throw new TypeError('offline'); if(path==='/api/chat/projects')return answer({rows:window.__projects}); if(path==='/api/chat/instances')return answer({rows:window.__instances}); if(path.includes('/rooms/')){ if(method==='POST'){ window.__writes.push({path,body:JSON.parse(init.body),redirect:init.redirect}); const code=path.endsWith('/open')?window.__openError:window.__sendError; if(code)return answer({error:code,message:'Untrusted reason',sent:false},409); if(window.__holdWrite)await new Promise(r=>window.__releaseWrite=r); if(window.__networkFail)throw new TypeError('offline'); if(path.endsWith('/open')){window.__readError=null;return answer({project:'${P}',threads:window.__threads.map(({id,instance,name})=>({id,instance,name}))});} const ids=['60000000-0000-4000-8000-000000000001','60000000-0000-4000-8000-000000000002']; window.__roomRows.push({id:ids[0],ids,from_role:'human',to:['bean','chet'],body:JSON.parse(init.body).body,at:'2026-10-24T17:00:00Z'}); return answer({ids,to:['bean','chet']}); } if(window.__readError)return answer({error:window.__readError,message:'Untrusted reason'},409); if(window.__holdRead){window.__signal=init.signal;await new Promise(r=>window.__releaseRead=r);} return answer({rows:window.__roomRows,after:window.__roomRows.at(-1)?.ids.at(-1)||null,hasMore:false}); } if(path.includes('/shares'))return answer({rows:[]}); if(path.endsWith('/sessions'))return answer({rows:[{id:'${T}',name:'default',default:true}]}); if(path.includes('/messages'))return answer({rows:[],after:null,hasMore:false}); if(path.startsWith('/api/chat/threads'))return answer({rows:window.__threads}); return answer({rows:[]}); }; })();`; async function setup(t, { path = `/chat/rooms/${P}`, before = '', suffix = '' } = {}) { const f = await fixture(); t.after(() => f.close()); const b = await browser(); t.after(() => b.close()); await b.call('Page.addScriptToEvaluateOnNewDocument', { source: stub + before }); await b.navigate(`${f.base}/${suffix}#${path}`); const wait = expression => b.evaluate(`(async()=>{for(let i=0;i<100;i++){if(${expression})return true;await new Promise(r=>setTimeout(r,30));}throw new Error(${JSON.stringify(expression)});})()`); const text = selector => b.evaluate(`document.querySelector(${JSON.stringify(selector)})?.textContent??null`); const set = value => b.evaluate(`(()=>{const el=document.querySelector('#chat-room-body');el.value=${JSON.stringify(value)};el.focus();el.setSelectionRange(el.value.length,el.value.length);el.dispatchEvent(new Event('input',{bubbles:true}));return true;})()`); await wait(path.includes('/rooms/') ? 'document.querySelector(".chat-room")' : 'document.querySelector("#chat-thread-form")'); return { b, wait, text, set }; } test('disabled room read shows unavailable history and disables opening', async t => { const { b, text } = await setup(t, { before: 'window.__readError="channel-disabled";' }); assert.match(await text('.chat-error'), /chat channel.*disabled/i); assert.match(await text('.chat-room-log'), /unavailable/i); assert.doesNotMatch(await text('.chat-room'), /No messages yet|Caught up to the last read/); assert.equal(await b.evaluate('document.querySelector("#chat-room-open").disabled'), true); assert.equal(await b.evaluate('window.__writes.length'), 0); }); test('room keeps a known disabled channel through a failed refresh and recovers on a successful read', async t => { const { b, wait, text } = await setup(t); const refresh = async () => { await wait('!document.querySelector("#refresh").disabled'); await b.evaluate('document.querySelector(".chat-area").dataset.beforeRefresh="yes";document.querySelector("#refresh").click();true'); await wait('!document.querySelector(".chat-area")?.dataset.beforeRefresh'); }; assert.match(await text('.chat-room-log'), /Broadcast once/); await b.evaluate('window.__readError="channel-disabled";true'); await refresh(); assert.match(await text('.chat-error'), /channel.*disabled/i); assert.equal(await b.evaluate('document.querySelector("#chat-room-open").disabled'), true); await b.evaluate('window.__readError=null;window.__chatOffline=true;true'); await refresh(); assert.equal(await b.evaluate('document.querySelector("#chat-room-open").disabled'), true); await b.evaluate('document.querySelector("#chat-room-open").dispatchEvent(new MouseEvent("click",{bubbles:true}));true'); assert.equal(await b.evaluate('window.__writes.length'), 0, 'the write guard also refuses a synthetic click'); assert.match(await text('.chat-room-log'), /Broadcast once/); await b.evaluate('window.__chatOffline=false;true'); await refresh(); assert.equal(await b.evaluate('document.querySelector("#chat-room-open").disabled'), false); assert.equal(await b.evaluate('window.__writes.length'), 0, 'recovery reads never open a room'); }); test('C2-10c room merges members, shows a broadcast once, reuses cited renderer and suppresses checked notice forgery', async t => { const { b, wait, text } = await setup(t); await wait('document.querySelector(".chat-cites")'); assert.equal(await b.evaluate('document.querySelectorAll(".chat-room-log > li[data-message]").length'), 5); assert.equal(await b.evaluate('document.querySelectorAll(".chat-room-log > li[data-message]")[0].dataset.message'), M); assert.match(await text('.chat-room-log > li:first-child .chat-message-meta'), /You.*Bean/); assert.equal(await text('.chat-scope'), 'supporting'); assert.equal(await text('.chat-agent-written'), 'agent-written: demo/bean'); assert.equal(await text('.chat-changed'), 'changed since'); assert.equal(await text('.chat-cites q'), HOSTILE); assert.equal(await b.evaluate('!!document.querySelector(".chat-room img") || !!window.injected'), false); assert.equal(await b.evaluate('document.querySelectorAll("[data-revert]").length'), 1); assert.equal(await b.evaluate('window.__writes.length'), 0, 'reading never opens a room or wakes an agent'); await b.evaluate('document.querySelector("[data-revert]").click();true'); await wait('document.querySelector(".chat-revert-receipt")?.textContent.startsWith("Reverted")'); assert.deepEqual(await b.evaluate('window.__writes[0].body'), { project: 'books', commit: SHA }); }); test('C2-10c explicit project Open room calls only room.open before navigating', async t => { const { b, wait } = await setup(t, { path: `/chat/projects/${P}` }); assert.equal(await b.evaluate('window.__writes.length'), 0); await b.evaluate('document.querySelector("#chat-room-open").click();true'); await wait('document.querySelector(".chat-room")'); assert.deepEqual(await b.evaluate('window.__writes'), [{ path: `/api/chat/rooms/${P}/open`, body: {}, redirect: 'manual' }]); }); test('C2-10c completion lists only room members, inserts instance tokens and preserves typed send exactly', async t => { const { b, wait, set } = await setup(t); await set('@be'); assert.deepEqual(await b.evaluate('[...document.querySelectorAll("[data-room-complete]")].map(e=>e.dataset.roomComplete)'), ['bean']); await b.key('Enter'); assert.equal(await b.evaluate('document.querySelector("#chat-room-body").value'), '@bean '); await set('@bean @ch'); await b.evaluate('document.querySelector("[data-room-complete=chet]").click();true'); assert.equal(await b.evaluate('document.querySelector("#chat-room-body").value'), '@bean @chet '); const body = ' \t@bean\n@chet unchanged text '; await set(body); await b.evaluate('document.querySelector("#chat-room-send-form").dispatchEvent(new Event("submit",{bubbles:true,cancelable:true}));true'); await wait('window.__writes.length===1 && !document.querySelector("#chat-room-body").disabled'); assert.deepEqual(await b.evaluate('window.__writes[0]'), { path: `/api/chat/rooms/${P}/messages`, body: { body }, redirect: 'manual' }); assert.equal(await b.evaluate('document.querySelector("#chat-room-body").value'), ''); assert.equal(await b.evaluate('document.querySelectorAll(".chat-room-log > li[data-message]").length'), 5, 'no optimistic copies of a broadcast'); await wait('!document.querySelector("#refresh").disabled'); await b.evaluate('document.querySelector("#refresh").click();true'); await wait('document.querySelectorAll(".chat-room-log > li[data-message]").length===6'); const sentSelector = '.chat-room-log > li[data-message="60000000-0000-4000-8000-000000000001"]'; assert.equal(await b.evaluate(`document.querySelectorAll(${JSON.stringify(sentSelector)}).length`), 1); }); test('C2-10c browser leading tag reader matches every broker parseAddress vector', async t => { const { b, set } = await setup(t); for (const [label, input, expected] of addressCases) { await set(input); assert.deepEqual(await b.evaluate('[...document.querySelectorAll("[data-room-tag]")].map(e=>e.dataset.roomTag)'), expected.tags, label); } await set('ordinary prose @be'); assert.equal(await b.evaluate('document.querySelectorAll("[data-room-complete]").length'), 0); assert.equal(await b.evaluate('window.__writes.length'), 0); }); test('C2-10c room refusals show fixed lines, preserve drafts and never retry', async t => { const { b, wait, set, text } = await setup(t); for (const code of ['unknown-addressee', 'addressee-ambiguous', 'room-not-open']) { await set('@unknown hello'); const count = await b.evaluate('window.__writes.length'); await b.evaluate(`window.__sendError=${JSON.stringify(code)};document.querySelector('#chat-room-send-form').dispatchEvent(new Event('submit',{bubbles:true,cancelable:true}));true`); await wait(`window.__writes.length===${count+1} && !document.querySelector('#chat-room-body').disabled`); assert.equal(await text('#chat-room-receipt'), refusals[code]); assert.equal(await b.evaluate('document.querySelector("#chat-room-body").value'), '@unknown hello'); } await b.evaluate("window.__openError='room-thread-name-taken';document.querySelector('#chat-room-open').click();true"); await wait(`document.querySelector('#chat-room-open-receipt')?.textContent===${JSON.stringify(refusals['room-thread-name-taken'])}`); assert.equal(await text('#chat-room-open-receipt'), refusals['room-thread-name-taken']); }); test('C2-10c read-only room does not offer a send or an implicit open', async t => { const { b, set } = await setup(t, { suffix: '?readonly' }); assert.equal(await b.evaluate('document.querySelector("#chat-room-body").disabled'), true); assert.equal(await b.evaluate('document.querySelector("#chat-room-open").disabled'), true); await set('not allowed'); await b.evaluate('document.querySelector("#chat-room-send-form").dispatchEvent(new Event("submit",{bubbles:true,cancelable:true}));true'); assert.equal(await b.evaluate('window.__writes.length'), 0); }); test('C2-10c late reads and sign-out cannot repaint private room state', async t => { const { b, wait, set } = await setup(t); await set('private room draft'); await wait('!document.querySelector("#refresh").disabled'); await b.evaluate('window.__holdRead=true;document.querySelector("#refresh").click();true'); await wait('typeof window.__releaseRead==="function"'); await b.evaluate(`location.hash='#/chat/projects/${Q}';true`); await wait('document.querySelector("#chat-thread-form")'); assert.equal(await b.evaluate('window.__signal.aborted'), true); await b.evaluate('window.__holdRead=false;window.__releaseRead();true'); await b.evaluate('document.dispatchEvent(new Event("mosaic-signed-out"));true'); assert.equal(await b.evaluate('!!document.querySelector(".chat-room")'), false); assert.equal(await b.evaluate('document.body.textContent.includes("private room draft")'), false); }); test('C2-10c quiet members complete in an empty room, and Escape closes suggestions', async t => { const { b, set } = await setup(t, { before: 'window.__roomRows=[];' }); await set('@'); assert.deepEqual(await b.evaluate('[...document.querySelectorAll("[data-room-complete]")].map(e=>e.dataset.roomComplete)'), ['bean', 'chet']); await b.key('ArrowDown'); await b.key('Tab'); assert.equal(await b.evaluate('document.querySelector("#chat-room-body").value'), '@chet '); await set('@b'); await b.key('Escape'); assert.equal(await b.evaluate('document.querySelector("#chat-room-completion").hidden'), true); assert.equal(await b.evaluate('window.__writes.length'), 0); }); test('C2-10c pending sends do not duplicate and uncertain sends preserve the exact draft', async t => { const { b, wait, set, text } = await setup(t); await set('one pending send'); await b.evaluate('window.__holdWrite=true;document.querySelector("#chat-room-send-form").dispatchEvent(new Event("submit",{bubbles:true,cancelable:true}));true'); await wait('typeof window.__releaseWrite==="function"'); await b.evaluate('document.querySelector("#chat-room-send-form").dispatchEvent(new Event("submit",{bubbles:true,cancelable:true}));true'); assert.equal(await b.evaluate('window.__writes.length'), 1); await b.evaluate('window.__holdWrite=false;window.__releaseWrite();true'); await wait('!document.querySelector("#chat-room-body").disabled'); await set('keep this uncertain draft '); await b.evaluate('window.__networkFail=true;document.querySelector("#chat-room-send-form").dispatchEvent(new Event("submit",{bubbles:true,cancelable:true}));true'); await wait('document.querySelector("#chat-room-receipt").textContent.includes("Outcome unknown")'); assert.equal(await b.evaluate('document.querySelector("#chat-room-body").value'), 'keep this uncertain draft '); assert.equal(await b.evaluate('window.__writes.length'), 2); assert.match(await text('#chat-room-receipt'), /Nothing was retried/); });