// Fixture-only browser coverage uses the repository's existing CDP test helper. import test from 'node:test'; import assert from 'node:assert/strict'; import { startServer } from '../src/serve.mjs'; import { fixture, close } from './fixture.mjs'; import { browser } from './browser.mjs'; const P = '10000000-0000-4000-8000-000000000001'; const A = '20000000-0000-4000-8000-000000000002'; const B = '30000000-0000-4000-8000-000000000003'; const M = '40000000-0000-4000-8000-000000000004'; const HOSTILE = '\u001b[31m\u202eevil'; const stub = `(() => { const real = fetch.bind(window); window.__writes=[];window.__reads=[];window.__answers=[];window.__pages=[]; window.__instances=[{instance:'assistant',name:'assistant'}]; window.__projects=[{id:'${P}', name:${JSON.stringify(HOSTILE)}, repo:null},{id:'\">',name:'Invalid record'}]; window.__sessions={}; window.__threads=[{id:'${A}',project:'${P}',name:'alpha',instance:'assistant'}, {id:'${B}',project:'${P}',name:'beta',instance:'assistant'}]; window.fetch=async (url,init={})=>{ const path=String(url), method=init.method||'GET'; if(path==='/api/config')return new Response(JSON.stringify({send:!location.search.includes('readonly')})); if(window.__busRefuse&&path.startsWith('/api/bus/'))return new Response(JSON.stringify({error:'human-required'}),{status:403}); if(path.startsWith('/api/bus/'))return new Response(JSON.stringify({rows:[],unread:0,at:new Date().toISOString()})); if(path==='/api/business')return new Response(JSON.stringify({projects:['stack']})); if(!path.startsWith('/api/chat'))return real(url,init); const data=value=>new Response(JSON.stringify({...value,at:new Date().toISOString()}),{headers:{'content-type':'application/json'}}); if(method==='POST'){ window.__writes.push({path,body:JSON.parse(init.body),redirect:init.redirect}); const a=window.__answers.shift();if(!a)throw new Error('No write answer'); if(a.hold)await new Promise(r=>window.__releaseWrite=r); if(a.reject)throw new TypeError('network'); if(a.redirect)return {type:'opaqueredirect',status:0,ok:false}; if(a.body?.state==='cancelled')window.__operation=a.body; return new Response(JSON.stringify(a.body),{status:a.status||200}); } window.__reads.push(path); if(window.__holdRead && path.includes(window.__holdRead)){window.__heldSignal=init.signal;await new Promise(r=>window.__releaseRead=r);} if(window.__readFailure && (!window.__failurePath || path.includes(window.__failurePath)))return new Response(JSON.stringify({error:window.__readFailure}),{status:['thread-not-owned','access-refused'].includes(window.__readFailure)?403:503}); if(path==='/api/chat/instances')return window.__instanceFailure ? new Response(JSON.stringify({error:'not-configured'}),{status:503}) : data({rows:window.__instances}); if(path==='/api/chat/projects')return data({rows:window.__projects}); if(path.endsWith('/sessions')){const t=path.split('/')[4];return data({rows:window.__sessions[t]||[{id:t,name:'default',default:true}]});} if(path.includes('/operation?'))return data(window.__operation); if(path.startsWith('/api/chat/threads')&&!path.includes('/messages'))return data({rows:window.__threads}); if(window.__pages.length)return data(window.__pages.shift()); return data({rows:[],after:null,hasMore:false}); }; })();`; async function setup(t, suffix = '', initial = '', startRoute = '#/chat') { const f = await fixture(); t.after(() => f.close()); const server = await startServer({ port: 0, board: f.boardURL }); t.after(() => close(server)); const b = await browser(); t.after(() => b.close()); await b.call('Page.addScriptToEvaluateOnNewDocument', { source: stub }); if (initial) await b.call('Page.addScriptToEvaluateOnNewDocument', { source: initial }); await b.navigate(`http://127.0.0.1:${server.address().port}/${suffix}${startRoute}`); const wait = expression => b.evaluate(`(async()=>{for(let i=0;i<100;i++){if(${expression})return;await new Promise(r=>setTimeout(r,30));}throw new Error(${JSON.stringify(expression)});})()`); const set = (selector, text) => b.evaluate(`(()=>{const el=document.querySelector(${JSON.stringify(selector)});el.value=${JSON.stringify(text)};el.dispatchEvent(new Event('input',{bubbles:true}));return true;})()`); const click = selector => b.evaluate(`document.querySelector(${JSON.stringify(selector)}).click(),true`); const route = async id => { await b.evaluate(`location.hash='#/chat/threads/${id}',true`); await wait(`document.querySelector('.chat-conversation')?.dataset.thread==='${id}'`); }; await wait('document.querySelector(".chat-projects")'); const refresh = async () => { // A successful read can leave the existing DOM in place. Await the real // renderer instead of using replacement of the old element as its signal. await b.evaluate(`MosaicChat.render({path:location.hash.slice(1),container:document.querySelector('#bus-view'),canSend:true,navigated:false})`); }; return { b, wait, set, click, route, refresh }; } test('chat browser: initial disabled channel reports unavailable lists and does not offer creation', async t => { const { b } = await setup(t, '', 'window.__readFailure="channel-disabled"'); const text = await b.evaluate('document.querySelector(".chat-area").textContent'); assert.match(text, /chat.*channel.*disabled/i); assert.doesNotMatch(text, /No projects yet|No threads yet|mosaic plugins|SENTINEL/i); assert.equal(await b.evaluate('document.querySelector("#chat-project-form button")?.disabled ?? true'), true); assert.equal(await b.evaluate('window.__writes.length'), 0); }); test('chat browser: refused creation keeps the draft, disables creation and recovers after refresh', async t => { const { b, wait, set, click, refresh } = await setup(t); await set('#chat-project-name', 'Pomodoro Timer'); await b.evaluate('window.__answers.push({status:503,body:{error:"channel-disabled",sent:false,message:"SENTINEL private transport"}}),true'); await click('#chat-project-form button[type=submit]'); await wait('document.querySelector("#chat-project-form [role=status]")?.textContent.includes("Not sent")'); assert.match(await b.evaluate('document.querySelector("#chat-project-form [role=status]").textContent'), /channel.*disabled/i); assert.doesNotMatch(await b.evaluate('document.querySelector(".chat-area").textContent'), /SENTINEL/); assert.equal(await b.evaluate('document.querySelector("#chat-project-name").value'), 'Pomodoro Timer'); assert.equal(await b.evaluate('document.querySelector("#chat-project-form button").disabled'), true); await refresh(); await wait('!document.querySelector("#chat-project-form button").disabled'); assert.equal(await b.evaluate('document.querySelector("#chat-project-name").value'), 'Pomodoro Timer'); assert.equal(await b.evaluate('window.__writes.length'), 1); await b.evaluate(`window.__answers.push({body:{id:'${P}'}}),true`); await click('#chat-project-form button[type=submit]'); await wait('document.querySelector("#chat-thread-form")'); assert.equal(await b.evaluate('window.__writes[1].body.name'), 'Pomodoro Timer'); }); test('chat browser: failed refresh marks cached lists stale; successful empty reads retain creation', async t => { const { b, wait, refresh } = await setup(t); await b.evaluate('window.__readFailure="channel-disabled";true'); await refresh(); await wait('document.querySelector(".chat-error")'); const failed = await b.evaluate('document.querySelector(".chat-area").textContent'); assert.match(failed, /channel.*disabled/i); assert.match(failed, /previous|cached|stale/i); assert.doesNotMatch(failed, /No projects yet|No threads yet/); assert.equal(await b.evaluate('document.querySelector("#chat-project-form button").disabled'), true); await b.evaluate('window.__readFailure=null;window.__projects=[];window.__threads=[];true'); await refresh(); await wait('document.querySelector(".chat-area").textContent.includes("No projects yet")'); assert.match(await b.evaluate('document.querySelector(".chat-area").textContent'), /No threads yet/); assert.equal(await b.evaluate('document.querySelector("#chat-project-form button").disabled'), false); assert.equal(await b.evaluate('window.__writes.length'), 0); }); test('chat browser: disabled thread creation retains its draft and refresh restores the action', async t => { const { b, wait, set, click, refresh } = await setup(t); await b.evaluate(`location.hash='#/chat/projects/${P}';true`); await wait('document.querySelector("#chat-thread-form button") && !document.querySelector("#chat-thread-form button").disabled'); await set('#chat-thread-name', 'planning'); await b.evaluate('window.__answers.push({status:503,body:{error:"channel-disabled",sent:false}}),true'); await click('#chat-thread-form button'); await wait('document.querySelector("#chat-thread-form [role=status]").textContent.includes("Not sent")'); assert.match(await b.evaluate('document.querySelector("#chat-thread-form [role=status]").textContent'), /channel.*disabled/i); assert.equal(await b.evaluate('document.querySelector("#chat-thread-name").value'), 'planning'); assert.equal(await b.evaluate('document.querySelector("#chat-thread-form button").disabled'), true); await refresh(); await wait('!document.querySelector("#chat-thread-form button").disabled'); assert.equal(await b.evaluate('document.querySelector("#chat-thread-name").value'), 'planning'); assert.equal(await b.evaluate('window.__writes.length'), 1); }); test('chat browser: uncertain disabled send remains unknown, preserves draft and does not retry', async t => { const { b, wait, set, click, route, refresh } = await setup(t); await route(A); await set('#chat-body', 'unsure draft'); await b.evaluate('window.__answers.push({status:503,body:{error:"channel-disabled",message:"SENTINEL"}}),true'); await click('#chat-send-form button'); await wait('document.querySelector("#chat-receipt").textContent.includes("Outcome unknown")'); assert.doesNotMatch(await b.evaluate('document.querySelector("#chat-receipt").textContent'), /Not sent|SENTINEL/); assert.equal(await b.evaluate('document.querySelector("#chat-body").value'), 'unsure draft'); assert.equal(await b.evaluate('document.querySelector("#chat-send-form button").disabled'), true); assert.equal(await b.evaluate('document.querySelector("#chat-session-form button").disabled'), true); await refresh(); await wait('!document.querySelector("#chat-send-form button").disabled'); assert.equal(await b.evaluate('document.querySelector("#chat-body").value'), 'unsure draft'); assert.equal(await b.evaluate('window.__writes.length'), 1); }); test('chat browser: a refused send explains the disabled channel and retains the message', async t => { const { b, wait, set, click, route } = await setup(t); await route(A); await set('#chat-body', 'keep my message'); await b.evaluate('window.__answers.push({status:503,body:{error:"channel-disabled",sent:false,message:"SENTINEL"}}),true'); await click('#chat-send-form button'); await wait('document.querySelector("#chat-receipt").textContent.includes("Not sent")'); const receipt = await b.evaluate('document.querySelector("#chat-receipt").textContent'); assert.match(receipt, /channel.*disabled/i); assert.doesNotMatch(receipt, /Outcome unknown|SENTINEL/); assert.equal(await b.evaluate('document.querySelector("#chat-body").value'), 'keep my message'); assert.equal(await b.evaluate('document.querySelector("#chat-send-form button").disabled'), true); assert.equal(await b.evaluate('window.__writes.length'), 1); }); test('chat browser: failed shared-thread read does not claim an empty shared list', async t => { const { b, wait, refresh } = await setup(t); await b.evaluate('window.__failurePath="shared=true";window.__readFailure="channel-disabled";true'); await refresh(); await wait('document.querySelector(".chat-error")'); const text = await b.evaluate('document.querySelector(".chat-area").textContent'); assert.match(text, /channel.*disabled/i); assert.doesNotMatch(text, /No threads yet/); }); test('chat browser: a shared-list refusal does not discard an owned thread draft', async t => { const { b, wait, set, route, refresh } = await setup(t); await route(A); await set('#chat-body', 'owned thread draft'); await b.evaluate('window.__failurePath="shared=true";window.__readFailure="access-refused";true'); await refresh(); await wait('document.querySelector(".chat-error")'); await b.evaluate('window.__readFailure=null;true'); await refresh(); await wait('document.querySelector("#chat-body")'); assert.equal(await b.evaluate('document.querySelector("#chat-body").value'), 'owned thread draft'); }); test('chat browser: a refused share explains the disabled channel without exposing transport text', async t => { const { b, wait, set, click, route } = await setup(t); await route(A); await set('#chat-share-target', 'user:reader'); await b.evaluate('window.__answers.push({status:503,body:{error:"channel-disabled",sent:false,message:"SENTINEL"}}),true'); await click('#chat-share-form button'); await wait('document.querySelector("#chat-share-form + [role=status]")?.textContent.includes("refused")'); const receipt = await b.evaluate('document.querySelector("#chat-share-form + [role=status]").textContent'); assert.match(receipt, /chat channel.*disabled/i); assert.doesNotMatch(receipt, /SENTINEL/); assert.equal(await b.evaluate('document.querySelector("#chat-share-target").value'), 'user:reader'); assert.equal(await b.evaluate('window.__writes.length'), 1); }); test('chat browser: session catalog recovery restores controls and scopes drafts and messages', async t => { const { b, wait, set, click, route, refresh } = await setup(t); await b.evaluate('window.__failurePath="/sessions";window.__readFailure="access-refused";true'); await route(A); assert.match(await b.evaluate('document.querySelector(".chat-area").textContent'), /sessions.*not.*available/i); await set('#chat-body', 'legacy draft'); await b.evaluate(`window.__readFailure=null;window.__sessions['${A}']=[{id:'${B}',name:'default',default:true,state:'active',revision:2}];window.__pages.push({rows:[{id:'${M}',body:'session history',from_role:'assistant',at:'2026-10-18T00:00:00Z'}],after:'${M}',hasMore:false});true`); await refresh(); assert.equal(await b.evaluate('document.querySelector("#chat-session")?.value'), B); assert.equal(await b.evaluate('!!document.querySelector("#chat-session-form") && !!document.querySelector("#chat-clear")'), true); assert.match(await b.evaluate('document.querySelector(".chat-log").textContent'), /session history/); assert.equal(await b.evaluate('document.querySelector("#chat-body").value'), '', 'legacy draft is not copied into another session'); await set('#chat-body', 'session draft'); await b.evaluate(`window.__answers.push({body:{id:'${P}'}}),true`); await click('#chat-send-form button'); await wait('document.querySelector("#chat-receipt").textContent.includes("Accepted")'); assert.deepEqual(await b.evaluate('window.__writes[0].body'), { body: 'session draft', session: B }); await b.evaluate('window.__readFailure="access-refused";true'); await refresh(); assert.equal(await b.evaluate('document.querySelector("#chat-body").value'), 'legacy draft'); }); test('chat browser: list/create named projects and threads, hostile labels inert and no implicit writes', async t => { const { b, wait, set, click } = await setup(t); assert.equal(await b.evaluate('window.__writes.length'), 0); assert.equal(await b.evaluate('window.injected'), undefined); assert.equal(await b.evaluate('!!document.querySelector(".chat-projects img")'), false); assert.match(await b.evaluate('document.querySelector(".chat-projects").textContent'), /\\u001b|␛/); const projectText = await b.evaluate('document.querySelector(".chat-projects").textContent'); assert.doesNotMatch(projectText, /[\u202a-\u202e\u2066-\u2069]/u); assert.match(projectText, /\\u202e/); assert.equal(await b.evaluate(`!!document.querySelector('#sections a[href="#/projects"]')`), true); await b.key('k', 'KeyK', 2); await wait('document.querySelector("#cmdk-dialog").open'); assert.equal(await b.evaluate(`!!document.querySelector('#cmdk-list [data-href="#/projects"]')`), true); await b.key('Escape'); await set('#chat-project-name', 'new project'); await set('#chat-repo', 'stack'); await b.evaluate(`window.__answers.push({body:{id:'${P}'}}),true`); await click('#chat-project-form button[type=submit]'); await wait('document.querySelector("#chat-thread-form")'); assert.deepEqual(await b.evaluate('window.__writes[0].body'), { name: 'new project', repo: 'stack' }); assert.equal(await b.evaluate('window.__writes[0].path'), '/api/chat/projects'); await set('#chat-thread-name', 'named thread'); await b.evaluate(`window.__answers.push({body:{id:'${A}',instance:'assistant'}}),true`); await click('#chat-thread-form button[type=submit]'); await wait('document.querySelector("#chat-send-form")'); assert.deepEqual(await b.evaluate('window.__writes[1].body'), { project: P, name: 'named thread', instance: 'assistant' }); assert.equal(await b.evaluate('window.__writes[1].path'), '/api/chat/threads'); assert.equal(await b.evaluate('window.__writes.length'), 2); }); test('chat browser: thread drafts, receipts and cursor history stay isolated; refresh never writes', async t => { const { b, wait, set, click, route, refresh } = await setup(t); await b.evaluate(`window.__pages.push({rows:[{id:'${M}',body:${JSON.stringify(HOSTILE)},from_role:'human',at:'2026-10-18T00:00:00Z'}],after:'${M}',hasMore:true}),true`); await route(A); assert.equal(await b.evaluate('!!document.querySelector(".chat-log img")'), false); const messageText = await b.evaluate('document.querySelector(".chat-log").textContent'); assert.doesNotMatch(messageText, /[\u202a-\u202e\u2066-\u2069]/u); assert.match(messageText, /\\u202e/); await set('#chat-body', 'alpha draft'); await b.evaluate('(()=>{const e=document.querySelector("#chat-body");e.focus();e.setSelectionRange(2,5);return true})()'); await refresh(); await wait('document.querySelector("#chat-body")?.value === "alpha draft" && !document.querySelector(".chat-loading")'); assert.deepEqual(await b.evaluate('(()=>{const e=document.querySelector("#chat-body");return [document.activeElement===e,e.selectionStart,e.selectionEnd]})()'), [true,2,5]); await route(B); await set('#chat-body','beta draft'); await route(A); assert.equal(await b.evaluate('document.querySelector("#chat-body").value'), 'alpha draft'); await b.evaluate(`window.__answers.push({body:{id:'${M}'}}),true`); await click('#chat-send-form button[type=submit]'); await wait('document.querySelector("#chat-receipt")?.textContent.includes("Accepted")'); assert.equal(await b.evaluate('document.querySelector("#chat-body").value'), ''); await route(B); assert.equal(await b.evaluate('document.querySelector("#chat-body").value'), 'beta draft'); assert.doesNotMatch(await b.evaluate('document.querySelector("#chat-receipt").textContent'), /Accepted/); assert.deepEqual(await b.evaluate('window.__writes.map(x=>x.body)'), [{body:'alpha draft',session:A}]); assert.equal(await b.evaluate('window.__writes[0].redirect'), 'manual'); assert.equal(await b.evaluate('window.__writes[0].path'), `/api/chat/threads/${A}/messages`); await b.evaluate('window.__answers.push({body:{opened:location.hash.split("/").at(-1)}}),true'); await click('#chat-resume'); await wait('document.querySelector("#chat-receipt")?.textContent.includes("requested")'); assert.equal(await b.evaluate('window.__writes.length'), 2); assert.equal(await b.evaluate('window.__writes[1].path'), `/api/chat/threads/${B}/open`); }); test('chat browser: uncertain writes retain draft, ownership refusal clears it, read-only cannot send', async t => { const { b, wait, set, click, route, refresh } = await setup(t); await route(A); await set('#chat-body', 'keep this'); await b.evaluate('window.__answers.push({reject:true}),true'); await click('#chat-send-form button[type=submit]'); await wait('document.querySelector("#chat-receipt")?.textContent.includes("unknown")'); assert.equal(await b.evaluate('document.querySelector("#chat-body").value'), 'keep this'); assert.equal(await b.evaluate('window.__writes.length'), 1); await b.evaluate('window.__readFailure="no-bus-host";true'); await refresh(); await wait('document.querySelector(".chat-error")'); assert.equal(await b.evaluate('document.querySelector("#chat-body").value'), 'keep this'); await wait('!document.querySelector("#refresh").disabled'); await b.evaluate('window.__readFailure="thread-not-owned";true'); await refresh(); await wait('document.querySelector(".chat-error")?.textContent.includes("not available")'); assert.equal(await b.evaluate('!!document.querySelector("#chat-body")'), false); await wait('!document.querySelector("#refresh").disabled'); await b.evaluate('window.__readFailure=null;true'); await refresh(); await wait('document.querySelector("#chat-body")'); assert.equal(await b.evaluate('document.querySelector("#chat-body").value'), ''); await b.navigate((await b.evaluate('location.href')).replace('/#/','/?readonly#/')); await wait('document.querySelector("#chat-body")'); assert.equal(await b.evaluate('document.querySelector("#chat-body").disabled'), true); await set('#chat-body','read-only must not submit'); await b.evaluate('document.querySelector("#chat-send-form").dispatchEvent(new Event("submit",{bubbles:true,cancelable:true}));true'); assert.equal(await b.evaluate('window.__writes.length'), 0); }); test('chat browser: a late read from A cannot paint B; sign-out clears private state and stops reads', async t => { const { b, wait, set, click, route, refresh } = await setup(t); await route(A); await set('#chat-body', 'private alpha'); await b.evaluate(`window.__holdRead='${A}/messages';document.querySelector('#refresh').click();true`); await wait('typeof window.__releaseRead === "function"'); await route(B); await set('#chat-body', 'private beta'); assert.equal(await b.evaluate('window.__heldSignal.aborted'), true); await b.evaluate('(async()=>{window.__holdRead=null;window.__readFailure="thread-not-owned";window.__releaseRead();await new Promise(r=>setTimeout(r,100));window.__readFailure=null;return true})()'); assert.equal(await b.evaluate('!!document.querySelector(".chat-error")'), false); assert.equal(await b.evaluate('document.querySelector(".chat-conversation").dataset.thread'), B); assert.equal(await b.evaluate('document.querySelector("#chat-body").value'), 'private beta'); await b.evaluate('window.__answers.push({hold:true,redirect:true}),true'); await click('#chat-send-form button[type=submit]'); await wait('typeof window.__releaseWrite === "function"'); await b.evaluate('document.dispatchEvent(new Event("mosaic-signed-out"));window.__releaseWrite();true'); await wait('document.body.classList.contains("signed-out")'); const count = await b.evaluate('window.__reads.length'); await b.evaluate('document.querySelector("#refresh").click();location.hash="#/chat";true'); await new Promise(r => setTimeout(r, 150)); assert.equal(await b.evaluate('window.__reads.length'), count); assert.doesNotMatch(await b.evaluate('document.body.innerText'), /private alpha|private beta/); assert.equal(await b.evaluate('window.__writes.length'), 1); }); test('chat browser: page forward and deduplicate replies; pending and byte-limit submissions do not duplicate', async t => { const { b, wait, set, click, route, refresh } = await setup(t); const row = { id: M, body: 'first', from_role: 'human', at: '2026-10-18T00:00:00Z' }; await b.evaluate(`window.__pages.push(${JSON.stringify({rows:[row],after:M,hasMore:true})}),true`); await route(A); const reply = { ...row, id: P, body: 'assistant reply', from_role: 'assistant' }; await b.evaluate(`window.__pages.push(${JSON.stringify({rows:[row,reply],after:P,hasMore:false})}),true`); await click('#chat-next'); await wait('document.querySelector(".chat-log")?.textContent.includes("assistant reply")'); assert.equal(await b.evaluate('document.querySelectorAll(".chat-log li").length'), 2); assert.ok((await b.evaluate('window.__reads')).some(x => x.includes('after=' + M))); await set('#chat-body', '界'.repeat(6000)); await click('#chat-send-form button[type=submit]'); await wait('document.querySelector("#chat-receipt")?.textContent.includes("too long")'); assert.equal(await b.evaluate('window.__writes.length'), 0); await set('#chat-body', 'one send'); await b.evaluate(`window.__answers.push({hold:true,body:{id:'${P}'}}),true`); await click('#chat-send-form button[type=submit]'); await wait('typeof window.__releaseWrite === "function"'); await b.evaluate('document.querySelector("#chat-send-form").dispatchEvent(new Event("submit",{bubbles:true,cancelable:true}));true'); assert.equal(await b.evaluate('window.__writes.length'), 1); await b.evaluate('window.__releaseWrite();true'); await wait('!document.querySelector("#chat-send-form button[type=submit]").disabled'); }); test('chat browser: scrolled history stays put, New messages and an own send return to the tail', async t => { const { b, wait, set, click, route, refresh } = await setup(t); const rows = Array.from({length:40}, (_,i)=>({id:`90000000-0000-4000-8000-${String(i).padStart(12,'0')}`,body:'history '+i+'\nsecond line',from_role:'assistant',at:'2026-10-18T00:00:00Z'})); await b.evaluate(`window.__pages.push(${JSON.stringify({rows,after:rows.at(-1).id,hasMore:false})}),true`); await route(A); await set('#chat-body','scrolled draft'); await b.evaluate('(()=>{const l=document.querySelector(".chat-log");l.scrollTop=100;l.dispatchEvent(new Event("scroll"));return true})()'); await refresh(); await wait('!document.querySelector("#chat-new").hidden'); assert.equal(await b.evaluate('document.querySelector(".chat-log").scrollTop'), 100); await click('#chat-new'); assert.equal(await b.evaluate('(()=>{const l=document.querySelector(".chat-log");return l.scrollTop+l.clientHeight>=l.scrollHeight-2})()'), true); await b.evaluate('(()=>{const l=document.querySelector(".chat-log");l.scrollTop=100;l.dispatchEvent(new Event("scroll"));return true})()'); await b.evaluate(`window.__answers.push({body:{id:'${M}'}}),true`); await click('#chat-send-form button[type=submit]'); await wait('document.querySelector("#chat-receipt").textContent.includes("Accepted")'); assert.equal(await b.evaluate('(()=>{const l=document.querySelector(".chat-log");return l.scrollTop+l.clientHeight>=l.scrollHeight-2})()'), true); }); test('chat browser: automatic polling follows a reply without a POST or a lost draft', {timeout:20000}, async t => { const { b, wait, set, route } = await setup(t); await route(A); await set('#chat-body','draft during reply'); await b.evaluate(`window.__pages.push(${JSON.stringify({rows:[{id:M,body:'automatic assistant reply',from_role:'assistant',at:'2026-10-18T00:00:00Z'}],after:M,hasMore:false})}),true`); await b.evaluate('(async()=>{for(let n=0;n<300;n++){if(document.querySelector(".chat-log").textContent.includes("automatic assistant reply"))return;await new Promise(r=>setTimeout(r,50));}throw new Error("poll did not follow reply")})()'); assert.equal(await b.evaluate('document.querySelector("#chat-body").value'), 'draft during reply'); assert.equal(await b.evaluate('window.__writes.length'), 0); }); test('chat browser: display stays bounded while every forward page remains reachable', async t => { const { b, wait, click, route } = await setup(t); const rows = Array.from({length:300}, (_,i)=>({id:`91000000-0000-4000-8000-${String(i).padStart(12,'0')}`,body:'bounded message '+i,from_role:'assistant',at:'2026-10-18T00:00:00Z'})); const pages = [0,100,200].map(start=>({rows:rows.slice(start,start+100),after:rows[start+99].id,hasMore:start<200})); await b.evaluate(`window.__pages.push(...${JSON.stringify(pages)}),true`); await route(A); await click('#chat-next'); await wait('document.querySelector(".chat-log").textContent.includes("bounded message 199")'); await click('#chat-next'); await wait('document.querySelector(".chat-log").textContent.includes("bounded message 299")'); assert.equal(await b.evaluate('document.querySelectorAll(".chat-log li").length'), 200); assert.doesNotMatch(await b.evaluate('document.querySelector(".chat-log").textContent'), /bounded message 0\n/); }); test('chat browser: a confirmed visible message replaces its pending receipt; malformed receipts stay unknown', async t => { const { b, wait, set, click, route, refresh } = await setup(t); await route(A); await set('#chat-body','sent then visible'); await b.evaluate(`window.__answers.push({body:{id:'${M}'}}),true`); await click('#chat-send-form button[type=submit]'); await wait('document.querySelector("#chat-receipt").textContent.includes("Accepted")'); await b.evaluate(`window.__pages.push(${JSON.stringify({rows:[{id:M,body:'sent then visible',from_role:'human'}],after:M,hasMore:false})});true`); await refresh(); await wait('document.querySelector("#chat-receipt").textContent.includes("shown")'); await set('#chat-body','keep malformed'); await b.evaluate('window.__answers.push({body:{}}),true'); await click('#chat-send-form button[type=submit]'); await wait('document.querySelector("#chat-receipt").textContent.includes("unknown")'); assert.equal(await b.evaluate('document.querySelector("#chat-body").value'), 'keep malformed'); assert.equal(await b.evaluate('window.__writes.length'), 2); }); test('chat browser: a refusal elsewhere forgets private chat drafts', async t => { const { b, wait, set, route } = await setup(t); await route(A); await set('#chat-body','private draft before refusal'); await b.evaluate('window.__busRefuse=true;location.hash="#/tasks";true'); await wait('document.querySelector("#bus-view")?.textContent.includes("refused")'); await b.evaluate('window.__busRefuse=false;true'); await route(A); assert.equal(await b.evaluate('document.querySelector("#chat-body").value'), ''); }); test('chat browser: a late project creation receipt cannot navigate away from another thread', async t => { const { b, wait, set, click, route, refresh } = await setup(t); await set('#chat-project-name','late project'); await b.evaluate(`window.__answers.push({hold:true,body:{id:'${P}'}}),true`); await click('#chat-project-form button[type=submit]'); await wait('typeof window.__releaseWrite === "function"'); await route(A); await set('#chat-body','stay in alpha'); await b.evaluate('(async()=>{window.__releaseWrite();await new Promise(r=>setTimeout(r,100));return true})()'); assert.equal(await b.evaluate('location.hash'), '#/chat/threads/' + A); assert.equal(await b.evaluate('document.querySelector("#chat-body").value'), 'stay in alpha'); }); test('C2-09a picker: no silent choice, duplicate hostile labels stay inert, selection survives refresh and reaches POST', async t => { const { b, wait, set, click, refresh } = await setup(t); await b.evaluate(`window.__instances=[{instance:'bean',name:${JSON.stringify(HOSTILE)}},{instance:'chet',name:${JSON.stringify(HOSTILE)}}];location.hash='#/chat/projects/${P}';true`); await wait('document.querySelectorAll("#chat-instance option").length===3'); assert.equal(await b.evaluate('document.querySelector("#chat-instance").value'), ''); assert.equal(await b.evaluate('document.querySelector("#chat-instance").required'), true); assert.equal(await b.evaluate('document.querySelector("#chat-thread-form button").disabled'), true); const labels = await b.evaluate('[...document.querySelectorAll("#chat-instance option")].map(o=>o.textContent)'); assert.match(labels[1], /\(bean\)/); assert.match(labels[2], /\(chet\)/); assert.doesNotMatch(labels.join(''), /[\u202a-\u202e\u2066-\u2069]/u); assert.equal(await b.evaluate('!!document.querySelector("#chat-thread-form img")'), false); assert.equal(await b.evaluate('window.injected'), undefined); await set('#chat-thread-name', 'Bean discussion'); await click('#chat-thread-form button'); assert.equal(await b.evaluate('window.__writes.length'), 0); await set('#chat-instance', 'bean'); await refresh(); assert.equal(await b.evaluate('document.querySelector("#chat-instance").value'), 'bean'); assert.equal(await b.evaluate('document.querySelector("#chat-thread-name").value'), 'Bean discussion'); assert.equal(await b.evaluate('document.querySelector("#chat-thread-form button").disabled'), false); await b.evaluate(`window.__answers.push({body:{id:'${A}',instance:'bean'}});true`); await click('#chat-thread-form button'); await wait('window.__writes.length===1'); assert.deepEqual(await b.evaluate('window.__writes[0]'), { path: '/api/chat/threads', body: { project: P, name: 'Bean discussion', instance: 'bean' }, redirect: 'manual' }); }); test('C2-09a picker: metadata failure, empty set and removed pick explain disabled state and recover without switching', async t => { const { b, wait, set, refresh } = await setup(t); await b.evaluate(`window.__instanceFailure=true;location.hash='#/chat/projects/${P}';true`); await wait('document.querySelector("#chat-thread-form") && !document.querySelector(".chat-loading")'); assert.match(await b.evaluate('document.querySelector("#chat-thread-form [role=status]").textContent'), /read the thread instances/); assert.equal(await b.evaluate('document.querySelector("#chat-thread-form button").disabled'), true); await b.evaluate('window.__instanceFailure=false;window.__instances=[];true'); await refresh(); assert.match(await b.evaluate('document.querySelector("#chat-thread-form [role=status]").textContent'), /No thread instances/); assert.equal(await b.evaluate('document.querySelector("#chat-thread-form button").disabled'), true); await b.evaluate("window.__instances=[{instance:'bean',name:'Bean'},{instance:'chet',name:'Chet'}];true"); await refresh(); await set('#chat-instance', 'bean'); await b.evaluate("window.__instances=[{instance:'chet',name:'Chet'}];true"); await refresh(); assert.equal(await b.evaluate('document.querySelector("#chat-instance").value'), ''); assert.match(await b.evaluate('document.querySelector("#chat-thread-form [role=status]").textContent'), /no longer available/); assert.equal(await b.evaluate('document.querySelector("#chat-thread-form button").disabled'), true); await refresh(); assert.equal(await b.evaluate('document.querySelector("#chat-instance").value'), '', 'another refresh must not silently select the one remaining instance'); await set('#chat-instance', 'chet'); assert.equal(await b.evaluate('document.querySelector("#chat-thread-form button").disabled'), false); }); test('C2-09a picker: a single instance is chosen explicitly but read-only mode cannot create a thread', async t => { const { b, wait } = await setup(t, '?readonly'); await b.evaluate(`location.hash='#/chat/projects/${P}';true`); await wait('document.querySelectorAll("#chat-instance option").length===2'); assert.equal(await b.evaluate('document.querySelector("#chat-instance").value'), 'assistant'); assert.equal(await b.evaluate('document.querySelector("#chat-instance").disabled'), true); assert.equal(await b.evaluate('document.querySelector("#chat-thread-form button").disabled'), true); assert.match(await b.evaluate('document.querySelector(".chat-area").textContent'), /only reads/); assert.equal(await b.evaluate('window.__writes.length'), 0); }); test('264e chooser: two sessions keep drafts, cursors, deep links and counts independent', async t => { const B = '30000000-0000-8000-8000-000000000003'; // mixed v4/v8 logical sessions const { b, wait, set, click, route, refresh } = await setup(t); await b.evaluate(`window.__sessions['${A}']=[{id:'${A}',name:'default',default:true,compactions:{known:true,count:3,reason:null}}, {id:'${B}',name:'second',default:false,compactions:{known:false,count:0,reason:'harness-unsupported'}}],true`); await route(A); assert.match(await b.evaluate('document.querySelector("#chat-compactions").textContent'), /Compactions: 3/); await set('#chat-body', 'first session draft'); await b.evaluate(`document.querySelector('#chat-session').value='${B}';document.querySelector('#chat-session').dispatchEvent(new Event('change',{bubbles:true}));true`); await wait(`document.querySelector('.chat-conversation')?.dataset.session==='${B}'`); assert.equal(await b.evaluate('document.querySelector("#chat-body").value'), ''); assert.match(await b.evaluate('document.querySelector("#chat-compactions").textContent'), /unknown.*harness-unsupported/); await set('#chat-body', 'second session draft'); await b.evaluate(`location.hash='#/chat/threads/${A}/sessions/${A}',true`); await wait(`document.querySelector('.chat-conversation')?.dataset.session==='${A}'`); assert.equal(await b.evaluate('document.querySelector("#chat-body").value'), 'first session draft'); await refresh(); assert.equal(await b.evaluate('document.querySelector("#chat-body").value'), 'first session draft'); assert.ok((await b.evaluate('window.__reads')).some(p => p.includes(`session=${B}`))); await b.evaluate(`window.__answers.push({body:{id:'${M}'}}),true`); await click('#chat-send-form button[type=submit]'); await wait('window.__writes.length===1'); assert.deepEqual(await b.evaluate('window.__writes[0].body'), {body:'first session draft',session:A}); assert.equal(await b.evaluate('document.querySelector("#chat-clear").disabled'), true); await b.evaluate(`location.hash='#/chat/threads/${A}/sessions/${M}',true`); await wait('document.querySelector(".chat-area h1")?.textContent === "Conversation unavailable"'); assert.equal(await b.evaluate('!!document.querySelector("#chat-send-form")'), false); assert.equal(await b.evaluate('window.__writes.length'), 1); }); test('264e clear confirmation and status never resend, force-stop or change selection', async t => { const { b, wait, click, route, refresh } = await setup(t); await b.evaluate(`window.__sessions['${A}']=[{id:'${A}',name:'default',default:true,state:'active',revision:2},{id:'${B}',name:'second',state:'active',revision:0}];window.confirm=()=>false;true`); await route(A); await click('#chat-clear'); assert.equal(await b.evaluate('window.__writes.length'), 0); await b.evaluate(`window.confirm=()=>true;window.__answers.push({body:{op:'placeholder',thread:'${A}',session:'${A}',successor:'${M}',state:'uncertain',start:null,refusal:null,at:null}});true`); // The fixture echoes the actual idempotency key, as the broker contract does. await b.evaluate(`(()=>{const real=window.fetch;window.fetch=async(u,i)=>{const r=await real(u,i);if(String(u).endsWith('/clear')){const v=await r.json();v.op=JSON.parse(i.body).op;window.__operation=v;return new Response(JSON.stringify(v));}return r;};return true;})()`); await click('#chat-clear'); await wait('document.querySelector("#chat-clear-receipt")?.textContent.includes("uncertain")'); const posted = await b.evaluate('window.__writes[0]'); assert.equal(posted.path, `/api/chat/threads/${A}/clear`); assert.deepEqual(Object.keys(posted.body).sort(), ['confirm','op','revision','session']); assert.equal(posted.body.session, A); assert.equal(posted.body.revision, 2); assert.equal(posted.body.confirm, true); await click('#chat-operation-form button'); await wait('window.__reads.some(p=>p.includes("/operation?"))'); await refresh(); assert.equal(await b.evaluate('window.__writes.length'), 1); assert.equal(await b.evaluate('document.querySelector(".chat-conversation").dataset.session'), A); }); test('264e new session keeps its operation key after an unknown response and uses no implicit open', async t => { const {b,wait,set,click,route}=await setup(t); await route(A); await set('#chat-session-name','new context'); await b.evaluate('window.__answers.push({reject:true}),true'); await click('#chat-session-form button'); await wait('document.querySelector("#chat-session-form [role=status]")?.textContent.includes("Outcome unknown")'); const first=await b.evaluate('window.__writes[0]'); assert.equal(first.path, `/api/chat/threads/${A}/sessions`); assert.equal(first.body.name,'new context'); assert.match(first.body.op,/^[a-f0-9-]{36}$/); // A deliberate retry uses the pinned payload; selecting a session never replays this write. await b.evaluate(`window.__answers.push({body:{id:'${B}'}});window.__sessions['${A}']=[{id:'${A}',name:'default',default:true},{id:'${B}',name:'new context'}];true`); await click('#chat-session-form button'); await wait(`document.querySelector('.chat-conversation')?.dataset.session==='${B}'`); assert.deepEqual(await b.evaluate('window.__writes[1].body'), first.body); assert.equal(await b.evaluate('window.__writes.length'),2); assert.equal(await b.evaluate('location.hash'),`#/chat/threads/${A}/sessions/${B}`); }); // org-lan-bind: a Console opened by LAN name is not a secure context, so crypto.randomUUID is // absent there. A new session still gets a random v4 operation key. test('a new session gets a v4 operation key without crypto.randomUUID', async t => { const {b,wait,set,click,route}=await setup(t,'','delete Crypto.prototype.randomUUID'); assert.equal(await b.evaluate('typeof crypto.randomUUID'),'undefined'); await route(A); await set('#chat-session-name','new context'); await b.evaluate(`window.__answers.push({body:{id:'${B}'}});window.__sessions['${A}']=[{id:'${A}',name:'default',default:true},{id:'${B}',name:'new context'}];true`); await click('#chat-session-form button'); await wait(`document.querySelector('.chat-conversation')?.dataset.session==='${B}'`); const write=await b.evaluate('window.__writes[0]'); assert.equal(write.path, `/api/chat/threads/${A}/sessions`); assert.match(write.body.op,/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/); }); test('264e cancel shows before/after retirement and never starts or replays work', async t => { const {b,wait,set,click,route}=await setup(t); await b.evaluate(`window.__sessions['${A}']=[{id:'${A}',name:'default',default:true,state:'active',revision:2}];window.confirm=()=>true;true`); await route(A); for (const retired of [null,true]) { const envelope={op:retired?'after-retirement':'before-retirement',thread:A,session:A,successor:M,state:'cancelled',start:retired?'held':null,retired,refusal:null,at:null}; await set('#chat-operation',envelope.op); await b.evaluate(`window.__operation=${JSON.stringify({...envelope,state:'uncertain'})};window.__answers.push({body:${JSON.stringify(envelope)}});true`); await click('#chat-cancel'); await b.evaluate(`window.__operation=${JSON.stringify(envelope)};true`); await wait(`document.querySelector('#chat-clear-receipt')?.textContent.includes('${retired?'startup cancelled':'old session active'}')`); const last=await b.evaluate('window.__writes.at(-1)'); assert.equal(last.path,`/api/chat/threads/${A}/cancel`); assert.deepEqual(last.body,{op:envelope.op,confirm:true}); assert.equal(await b.evaluate('document.querySelector("#chat-cancel").disabled'),true); } assert.equal(await b.evaluate('window.__writes.length'),2); assert.equal(await b.evaluate('window.__writes.some(w=>/\\/(open|messages|sessions|clear)$/.test(w.path))'),false); }); test('272 B1: a bad-session deep link preserves the draft and lost clear operation on A', async t => { const {b,wait,set,click,route}=await setup(t); await b.evaluate(`window.__sessions['${A}']=[{id:'${A}',name:'default',default:true,state:'active',revision:2},{id:'${B}',name:'sibling',state:'active',revision:0}];window.confirm=()=>true;true`); await route(A); await set('#chat-body','keep A draft through invalid session'); await b.evaluate('window.__answers.push({reject:true}),true'); await click('#chat-clear'); await wait('window.__writes.length===1 && !document.querySelector("#chat-clear").disabled'); const op=await b.evaluate('window.__writes[0].body.op'); assert.equal(await b.evaluate('document.querySelector("#chat-operation").value'),op); await b.evaluate(`location.hash='#/chat/threads/${A}/sessions/${M}',true`); await wait('document.querySelector(".chat-area h1")?.textContent === "Conversation unavailable"'); assert.equal(await b.evaluate('!!document.querySelector("#chat-send-form")'),false); await b.evaluate(`location.hash='#/chat/threads/${A}/sessions/${A}',true`); await wait(`document.querySelector('.chat-conversation')?.dataset.session==='${A}'`); assert.equal(await b.evaluate('document.querySelector("#chat-body").value'),'keep A draft through invalid session'); assert.equal(await b.evaluate('document.querySelector("#chat-operation").value'),op); await click('#chat-clear'); await wait('window.__reads.some(p=>p.includes("/operation?"))'); assert.equal(await b.evaluate('window.__writes.length'),1,'a retained operation is queried, never replaced with a new clear'); }); test('archive navigation exposes saved threads and unarchive with shared button styling', async t => { const initial = `window.MutationObserver=class { observe(){} disconnect(){} };window.__projects=[];window.__archiveRows=[{id:'${P}',name:${JSON.stringify(HOSTILE)},repo:null,archived:true}]; window.confirm=()=>true; window.__sessions['${A}']=[{id:'${A}',name:'default',default:true,state:'active',revision:2}]; window.__operation={op:'pre-archive',thread:'${A}',session:'${A}',successor:'${M}',state:'retired',start:null,refusal:null,at:null}; const base=window.fetch;window.fetch=async(url,opts)=>String(url)==='/api/chat/projects/archived'?new Response(JSON.stringify({rows:window.__archiveRows})):base(url,opts);`; const { b, wait, click, set } = await setup(t, '', initial, '#/projects'); await wait('document.querySelector("#project-archive-toggle")'); assert.equal(await b.evaluate('document.querySelector("#project-archive-list").children.length'), 0); await click('#project-archive-toggle'); await wait('document.querySelector("#project-archive-list a")'); assert.equal(await b.evaluate('document.querySelector("#project-archive-list a strong").textContent'), HOSTILE); assert.equal(await b.evaluate('window.injected??null'), null); const hooks = await b.evaluate('Array.from(document.querySelectorAll(".project-archived-filter, .project-archived-filter [class]")).flatMap(el=>Array.from(el.classList))'); assert.ok(hooks.includes('project-card') && hooks.includes('btn'), JSON.stringify(hooks)); assert.equal(await b.evaluate('!!document.querySelector("#project-archive-list a button")'), false); assert.equal(await b.evaluate('document.querySelector("#project-archive-list a").getAttribute("href")'), `#/projects/${P}`); await click('#project-archive-list a'); await wait('document.querySelector(".project-archive-control")'); assert.equal(await b.evaluate('document.querySelector(".chat-area h1").textContent'), '\\u001b[31m\\u202eevil'); assert.equal(await b.evaluate(`new Set([...document.querySelectorAll('a[href*="${A}"],a[href*="${B}"]')].map(a=>a.getAttribute("href"))).size`), 2); assert.equal(await b.evaluate('document.querySelector("#chat-thread-form")'), null); assert.equal(await b.evaluate('document.querySelector("#chat-room-open")'), null); assert.equal(await b.evaluate('document.querySelector("#project-archive-button").textContent'), 'Unarchive'); await click(`a[href="#/projects/rooms/${P}"]`); await wait('document.querySelector(".chat-room")'); assert.equal(await b.evaluate('document.querySelector("#chat-room-send-form")'), null); assert.equal(await b.evaluate('document.querySelector("#chat-room-open")'), null); await click(`.chat-room a[href="#/projects/${P}"]`); await wait('document.querySelector("#project-archive-button")'); await click(`a[href="#/projects/threads/${A}"]`); await wait('document.querySelector(".chat-conversation")'); assert.equal(await b.evaluate('document.querySelector("#chat-resume")'), null); assert.equal(await b.evaluate('document.querySelector("#chat-send-form")'), null); assert.ok(await b.evaluate('!!document.querySelector("#chat-session")')); assert.equal(await b.evaluate('document.querySelector("#chat-clear").disabled'), true); await set('#chat-operation', 'pre-archive'); await click('#chat-operation-form button'); await wait('document.querySelector("#chat-clear-receipt")?.textContent.includes("retired")'); assert.equal(await b.evaluate('document.querySelector("#chat-cancel").disabled'), true); await b.evaluate(`location.hash='#/chat/projects/${P}';true`); await wait(`document.querySelector('a[href="#/chat/rooms/${P}"]')`); assert.ok(await b.evaluate(`!!document.querySelector('a[href="#/chat/rooms/${P}"]')`)); await b.evaluate(`location.hash='#/projects/${P}';true`); await wait('document.querySelector("#project-archive-button")'); await b.evaluate(`window.__projects=[{id:'${P}',name:${JSON.stringify(HOSTILE)},repo:null}];window.__archiveRows=[];window.__answers.push({body:{project:'${P}',archived:false}});true`); await click('#project-archive-button'); await wait('window.__writes.at(-1)?.path.endsWith("/unarchive")'); await b.evaluate(`location.hash='#/projects/${P}/settings'`); await wait('document.querySelector("#project-archive-button")?.textContent==="Archive project"'); assert.equal(await b.evaluate('window.__writes.at(-1).path'), `/api/chat/projects/${P}/unarchive`); await b.evaluate(`location.hash='#/projects/${P}/conversations'`); await wait('!!document.querySelector("#chat-thread-form")'); });